diff --git a/Workflow/Sources/AnyWorkflow.swift b/Workflow/Sources/AnyWorkflow.swift index cb19086ae..a142b534b 100644 --- a/Workflow/Sources/AnyWorkflow.swift +++ b/Workflow/Sources/AnyWorkflow.swift @@ -18,17 +18,24 @@ public struct AnyWorkflow { private let storage: AnyStorage + /// The underlying erased workflow instance + public var base: Any { storage.base } + private init(storage: AnyStorage) { self.storage = storage } /// Initializes a new type-erased wrapper for the given workflow. public init(_ workflow: T) where T.Rendering == Rendering, T.Output == Output { - self.init(storage: Storage( - workflow: workflow, - renderingTransform: { $0 }, - outputTransform: { $0 } - )) + if let workflow = workflow as? AnyWorkflow { + self = workflow + } else { + self.init(storage: Storage( + workflow: workflow, + renderingTransform: { $0 }, + outputTransform: { $0 } + )) + } } /// The underlying workflow's implementation type. @@ -37,7 +44,17 @@ public struct AnyWorkflow { } } -extension AnyWorkflow: AnyWorkflowConvertible { +extension AnyWorkflow: Workflow { + public typealias Output = Output + public typealias State = Void + public typealias Rendering = Rendering + + public func render(state: Void, context: RenderContext>) -> Rendering { + storage.render(context: context, key: "") { + AnyWorkflowAction(sendingOutput: $0) + } + } + public func asAnyWorkflow() -> AnyWorkflow { return self } @@ -84,6 +101,8 @@ extension AnyWorkflow { /// /// This type is never used directly. fileprivate class AnyStorage { + var base: Any { fatalError() } + func render(context: RenderContext, key: String, outputMap: @escaping (Output) -> Action) -> Rendering where Action: WorkflowAction, Action.WorkflowType == Parent { fatalError() } @@ -115,6 +134,8 @@ extension AnyWorkflow { self.outputTransform = outputTransform } + override var base: Any { workflow } + override var workflowType: Any.Type { return T.self } diff --git a/Workflow/Tests/AnyWorkflowTests.swift b/Workflow/Tests/AnyWorkflowTests.swift index 7b811e1b9..e72a22f43 100644 --- a/Workflow/Tests/AnyWorkflowTests.swift +++ b/Workflow/Tests/AnyWorkflowTests.swift @@ -54,6 +54,34 @@ public class AnyWorkflowTests: XCTestCase { } wait(for: [renderingExpectation, outputExpectation], timeout: 1) } + + func testOnlyWrapsOnce() { + // direct initializer + do { + let base = OnOutputWorkflow() + let wrappedOnce = AnyWorkflow(base) + let wrappedTwice = AnyWorkflow(wrappedOnce) + + XCTAssertNotNil(wrappedOnce.base as? OnOutputWorkflow) + XCTAssertNotNil(wrappedTwice.base as? OnOutputWorkflow) + } + + // method chaining + do { + let base = OnOutputWorkflow() + let wrappedOnce = base.asAnyWorkflow() + let wrappedTwice = base.asAnyWorkflow().asAnyWorkflow() + + XCTAssertNotNil(wrappedOnce.base as? OnOutputWorkflow) + XCTAssertNotNil(wrappedTwice.base as? OnOutputWorkflow) + } + } + + func testBaseValue() { + let erased = OnOutputWorkflow().asAnyWorkflow() + + XCTAssertNotNil(erased.base as? OnOutputWorkflow) + } } /// Has no state or output, simply renders a reversed string diff --git a/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift b/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift index 0000aa29a..8ab60518b 100644 --- a/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift +++ b/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift @@ -109,6 +109,40 @@ final class WorkflowRenderTesterTests: XCTestCase { .assertNoOutput() } + func test_ignoredOutput_opaqueChild() { + OpaqueChildOutputIgnoringWorkflow( + childProvider: { + OutputWorkflow() + .mapRendering { _ in "screen" } + .asAnyWorkflow() + } + ) + .renderTester() + .expectWorkflowIgnoringOutput( + type: AnyWorkflow.self, + producingRendering: "test" + ) + .render { rendering in + XCTAssertEqual(rendering, "test") + } + } + + func test_opaqueChild() { + OpaqueChildWorkflow( + childProvider: { + MockChildWorkflow().asAnyWorkflow() + } + ) + .renderTester() + .expectWorkflow( + type: MockChildWorkflow.self, + producingRendering: "test" + ) + .render { rendering in + XCTAssertEqual(rendering, "test") + } + } + func test_childWorkflow() { ParentWorkflow(initialText: "hello") .renderTester() @@ -262,6 +296,41 @@ private struct OutputIgnoringWorkflow: Workflow { } } +private struct MockChildWorkflow: Workflow { + typealias State = Void + typealias Rendering = String + + func render(state: Void, context: RenderContext) -> String { + XCTFail("should never be rendered") + return "" + } +} + +private struct OpaqueChildWorkflow: Workflow { + typealias State = Void + typealias Rendering = String + + var childProvider: () -> AnyWorkflow + + func render(state: Void, context: RenderContext) -> Rendering { + childProvider() + .rendered(in: context) + } +} + +private struct OpaqueChildOutputIgnoringWorkflow: Workflow { + typealias State = Void + typealias Rendering = String + + var childProvider: () -> AnyWorkflow + + func render(state: Void, context: RenderContext) -> Rendering { + childProvider() + .ignoringOutput() + .rendered(in: context) + } +} + private struct TestSideEffectKey: Hashable { let key: String = "Test Side Effect" } diff --git a/WorkflowUI/Sources/Hosting/WorkflowHostingController.swift b/WorkflowUI/Sources/Hosting/WorkflowHostingController.swift index 8d196bb99..0fecc2a40 100644 --- a/WorkflowUI/Sources/Hosting/WorkflowHostingController.swift +++ b/WorkflowUI/Sources/Hosting/WorkflowHostingController.swift @@ -29,7 +29,7 @@ public final class WorkflowHostingController: UIViewControll private(set) var rootViewController: UIViewController - private let workflowHost: WorkflowHost> + private let workflowHost: WorkflowHost> private let (lifetime, token) = Lifetime.make() @@ -45,7 +45,7 @@ public final class WorkflowHostingController: UIViewControll observers: [WorkflowObserver] = [] ) where W.Rendering == ScreenType, W.Output == Output { self.workflowHost = WorkflowHost( - workflow: RootWorkflow(workflow), + workflow: workflow.asAnyWorkflow(), observers: observers ) @@ -74,7 +74,7 @@ public final class WorkflowHostingController: UIViewControll /// Updates the root Workflow in this container. public func update(workflow: W) where W.Rendering == ScreenType, W.Output == Output { - workflowHost.update(workflow: RootWorkflow(workflow)) + workflowHost.update(workflow: workflow.asAnyWorkflow()) } public required init?(coder aDecoder: NSCoder) { @@ -150,25 +150,4 @@ public final class WorkflowHostingController: UIViewControll } } -/// Wrapper around an AnyWorkflow that allows us to have a concrete -/// WorkflowHost without WorkflowHostingController itself being generic -/// around a Workflow. -fileprivate struct RootWorkflow: Workflow { - typealias State = Void - typealias Output = Output - typealias Rendering = Rendering - - var wrapped: AnyWorkflow - - init(_ wrapped: W) where W.Rendering == Rendering, W.Output == Output { - self.wrapped = wrapped.asAnyWorkflow() - } - - func render(state: State, context: RenderContext) -> Rendering { - return wrapped - .mapOutput { AnyWorkflowAction(sendingOutput: $0) } - .rendered(in: context) - } -} - #endif