diff --git a/WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift b/WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift index ece3684dc..450063694 100644 --- a/WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift +++ b/WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift @@ -99,6 +99,55 @@ public struct ViewControllerDescription { } } + /// Constructs a view controller description by providing closures used to + /// build and update a specific view controller type. This initializer supports + /// implementations that cannot rely on static types. + /// + /// - Parameters: + /// - performInitialUpdate: If an initial call to `update(viewController:)` + /// will be performed when the view controller is created. Defaults to `true`. + /// + /// - dynamicType: The type of view controller produced by this description. + /// This type is used by `KindIdentifier` and its `checkViewControllerType` + /// closure to inspect view controller types at runtime. + /// + /// - environment: The `ViewEnvironment` that should be injected above the + /// described view controller for ViewEnvironmentUI environment propagation. + /// This is typically passed in from a `Screen` in its + /// `viewControllerDescription(environment:)` method. + /// + /// - build: Closure that produces a new instance of the view controller + /// + /// - update: Closure that updates the given view controller + public init( + performInitialUpdate: Bool = true, + dynamicType: UIViewController.Type, + environment: ViewEnvironment, + build: @escaping () -> UIViewController, + update: @escaping (UIViewController) -> Void + ) { + self.performInitialUpdate = performInitialUpdate + + self.kind = .init(dynamicType: dynamicType) + + self.environment = environment + + self.build = { + let viewController = build() + guard viewController.isKind(of: dynamicType) else { + fatalError("Error creating \(viewController), expecting a \(dynamicType)") + } + return viewController + } + + self.update = { untypedViewController in + guard untypedViewController.isKind(of: dynamicType) else { + fatalError("Unable to update \(untypedViewController), expecting a \(dynamicType)") + } + update(untypedViewController) + } + } + /// Construct and update a new view controller as described by this view controller description. /// The view controller will be updated before it is returned, so it is fully configured and prepared for display. public func buildViewController() -> UIViewController { @@ -197,6 +246,12 @@ extension ViewControllerDescription { self.checkViewControllerType = { $0 is VC } } + /// This initializer uses dynamic type inspection in `checkViewControllerType`. + fileprivate init(dynamicType: UIViewController.Type) { + self.viewControllerType = dynamicType + self.checkViewControllerType = { $0.isKind(of: dynamicType) } + } + /// If the given view controller is of the correct type to be updated by this view controller description. /// /// If your view controller type can change between updates, call this method before invoking `update(viewController:)`. diff --git a/WorkflowUI/Tests/ViewControllerDescriptionTests.swift b/WorkflowUI/Tests/ViewControllerDescriptionTests.swift index dbe289a53..f5e227d58 100644 --- a/WorkflowUI/Tests/ViewControllerDescriptionTests.swift +++ b/WorkflowUI/Tests/ViewControllerDescriptionTests.swift @@ -23,6 +23,8 @@ import Workflow @testable import WorkflowUI fileprivate class BlankViewController: UIViewController {} +fileprivate class BlankViewControllerSubclass: BlankViewController {} +fileprivate class SecondBlankViewController: UIViewController {} @objc fileprivate protocol MyProtocol { func update() @@ -203,6 +205,76 @@ class ViewControllerDescriptionTests: XCTestCase { let viewControllerAgain = description.buildViewController() XCTAssertFalse(viewController === viewControllerAgain) } + + func test_viewControllerTypes() { + func polymorphicController() -> UIViewController { BlankViewController() } + let controller = polymorphicController() + + // Case 1: Dynamic type inspection. + // kind.viewControllerType == BlankViewController.self. + let descriptionWithDynamicType = ViewControllerDescription( + dynamicType: type(of: controller), + environment: .empty, + build: { controller }, + update: { _ in } + ) + // canUpdate(viewController:) will evaluate to true for matching UIViewController types or subclasses. + XCTAssertTrue(descriptionWithDynamicType.canUpdate(viewController: polymorphicController())) + XCTAssertTrue(descriptionWithDynamicType.canUpdate(viewController: BlankViewController())) + XCTAssertFalse(descriptionWithDynamicType.canUpdate(viewController: UIViewController())) + XCTAssertFalse(descriptionWithDynamicType.canUpdate(viewController: SecondBlankViewController())) + XCTAssertTrue(descriptionWithDynamicType.canUpdate(viewController: BlankViewControllerSubclass())) + + // Case 2: Example of static types losing granularity when supplying a superclass type. + // kind.viewControllerType == UIViewController.self. + let descriptionWithStaticSupertype = ViewControllerDescription( + type: type(of: controller), + environment: .empty, + build: { controller }, + update: { _ in } + ) + // canUpdate(viewController:) evaluates to true for any UIViewController type. + XCTAssertTrue(descriptionWithStaticSupertype.canUpdate(viewController: polymorphicController())) + XCTAssertTrue(descriptionWithStaticSupertype.canUpdate(viewController: BlankViewController())) + XCTAssertTrue(descriptionWithStaticSupertype.canUpdate(viewController: UIViewController())) + XCTAssertTrue(descriptionWithStaticSupertype.canUpdate(viewController: SecondBlankViewController())) + XCTAssertTrue(descriptionWithStaticSupertype.canUpdate(viewController: BlankViewControllerSubclass())) + + // Case 3: Common/standard Workflow use case with static types. + // kind.viewControllerType == BlankViewController.self. + let descriptionWithStaticSubtype = ViewControllerDescription( + type: BlankViewController.self, + environment: .empty, + build: { BlankViewController() }, + update: { _ in } + ) + // canUpdate(viewController:) will evaluate to true for matching UIViewController types or subclasses. + XCTAssertTrue(descriptionWithStaticSubtype.canUpdate(viewController: polymorphicController())) + XCTAssertTrue(descriptionWithStaticSubtype.canUpdate(viewController: BlankViewController())) + XCTAssertFalse(descriptionWithStaticSubtype.canUpdate(viewController: UIViewController())) + XCTAssertFalse(descriptionWithStaticSubtype.canUpdate(viewController: SecondBlankViewController())) + XCTAssertTrue(descriptionWithStaticSubtype.canUpdate(viewController: BlankViewControllerSubclass())) + } + + func test_buildingWithDynamicType() { + let descriptionWithSuperclassType = ViewControllerDescription( + dynamicType: BlankViewController.self, + environment: .empty, + build: { BlankViewControllerSubclass() }, + update: { _ in } + ) + XCTAssertTrue(descriptionWithSuperclassType.buildViewController().isKind(of: BlankViewController.self)) + XCTAssertTrue(descriptionWithSuperclassType.buildViewController().isKind(of: BlankViewControllerSubclass.self)) + + let descriptionWithExactType = ViewControllerDescription( + dynamicType: BlankViewController.self, + environment: .empty, + build: { BlankViewController() }, + update: { _ in } + ) + XCTAssertTrue(descriptionWithExactType.buildViewController().isKind(of: BlankViewController.self)) + XCTAssertFalse(descriptionWithExactType.buildViewController().isKind(of: BlankViewControllerSubclass.self)) + } } class ViewControllerDescription_KindIdentifierTests: XCTestCase {