Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,56 @@ 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
@_spi(DynamicControllerTypes)
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 {
Expand Down Expand Up @@ -197,6 +247,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:)`.
Expand Down
74 changes: 73 additions & 1 deletion WorkflowUI/Tests/ViewControllerDescriptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import XCTest

import ReactiveSwift
import Workflow
@testable import WorkflowUI
@_spi(DynamicControllerTypes) @testable import WorkflowUI

fileprivate class BlankViewController: UIViewController {}
fileprivate class BlankViewControllerSubclass: BlankViewController {}
fileprivate class SecondBlankViewController: UIViewController {}

@objc fileprivate protocol MyProtocol {
func update()
Expand Down Expand Up @@ -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 {
Expand Down
Loading