From 9be6a821f08dc48f1a350f4876a7759699f48c9f Mon Sep 17 00:00:00 2001 From: jamieQ Date: Sat, 14 Jan 2023 14:16:36 -0600 Subject: [PATCH 1/6] [WIP]: adds Workflow conformance to AnyWorkflow --- Workflow/Sources/AnyWorkflow.swift | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Workflow/Sources/AnyWorkflow.swift b/Workflow/Sources/AnyWorkflow.swift index cb19086ae..a75bc50ec 100644 --- a/Workflow/Sources/AnyWorkflow.swift +++ b/Workflow/Sources/AnyWorkflow.swift @@ -24,11 +24,17 @@ public struct AnyWorkflow { /// 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 } - )) + switch workflow as? AnyWorkflow { + case let anyWorkflow?: + self = anyWorkflow + + case nil: + self.init(storage: Storage( + workflow: workflow, + renderingTransform: { $0 }, + outputTransform: { $0 } + )) + } } /// The underlying workflow's implementation type. @@ -37,7 +43,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 } From eb623fb4f3d1e9d4d3ab53d700b39ed7255a2b1c Mon Sep 17 00:00:00 2001 From: jamieQ Date: Tue, 24 Jan 2023 15:42:17 -0600 Subject: [PATCH 2/6] cleanup & tests --- .../Tests/WorkflowRenderTesterTests.swift | 31 ++++++++++++++++++ .../Hosting/WorkflowHostingController.swift | 32 ++++--------------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift b/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift index 0000aa29a..8330c8a50 100644 --- a/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift +++ b/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift @@ -109,6 +109,24 @@ 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_childWorkflow() { ParentWorkflow(initialText: "hello") .renderTester() @@ -262,6 +280,19 @@ private struct OutputIgnoringWorkflow: Workflow { } } +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 93bf7a0f0..68b276610 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() @@ -39,8 +39,11 @@ public final class WorkflowHostingController: UIViewControll } } - public init(workflow: W, rootViewEnvironment: ViewEnvironment = .empty) where W.Rendering == ScreenType, W.Output == Output { - self.workflowHost = WorkflowHost(workflow: RootWorkflow(workflow)) + public init( + workflow: W, + rootViewEnvironment: ViewEnvironment = .empty + ) where W.Rendering == ScreenType, W.Output == Output { + self.workflowHost = WorkflowHost(workflow: workflow.asAnyWorkflow()) self.rootViewController = workflowHost .rendering @@ -67,7 +70,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) { @@ -144,27 +147,6 @@ 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) - } -} - @available(*, deprecated, renamed: "WorkflowHostingController") public typealias ContainerViewController = WorkflowHostingController From 67393fe365c045135911315130c81dbc41962de6 Mon Sep 17 00:00:00 2001 From: jamieQ Date: Fri, 27 Jan 2023 16:32:51 -0600 Subject: [PATCH 3/6] more tests --- .../Tests/WorkflowRenderTesterTests.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift b/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift index 8330c8a50..8ab60518b 100644 --- a/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift +++ b/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift @@ -127,6 +127,22 @@ final class WorkflowRenderTesterTests: XCTestCase { } } + 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() @@ -280,6 +296,28 @@ 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 From 296893dcc0674e4cf768e76e1f9c8a8aec0adf48 Mon Sep 17 00:00:00 2001 From: jamieQ Date: Fri, 24 Feb 2023 21:11:02 -0600 Subject: [PATCH 4/6] expose erased workflow property in AnyWorkflow --- Workflow/Sources/AnyWorkflow.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Workflow/Sources/AnyWorkflow.swift b/Workflow/Sources/AnyWorkflow.swift index a75bc50ec..a142b534b 100644 --- a/Workflow/Sources/AnyWorkflow.swift +++ b/Workflow/Sources/AnyWorkflow.swift @@ -18,17 +18,18 @@ 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 { - switch workflow as? AnyWorkflow { - case let anyWorkflow?: - self = anyWorkflow - - case nil: + if let workflow = workflow as? AnyWorkflow { + self = workflow + } else { self.init(storage: Storage( workflow: workflow, renderingTransform: { $0 }, @@ -100,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() } @@ -131,6 +134,8 @@ extension AnyWorkflow { self.outputTransform = outputTransform } + override var base: Any { workflow } + override var workflowType: Any.Type { return T.self } From 3e9b3a4b731cacfccf551842856e7d33fd29d60c Mon Sep 17 00:00:00 2001 From: jamieQ Date: Fri, 24 Feb 2023 21:22:45 -0600 Subject: [PATCH 5/6] tests --- Workflow/Tests/AnyWorkflowTests.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Workflow/Tests/AnyWorkflowTests.swift b/Workflow/Tests/AnyWorkflowTests.swift index 7b811e1b9..2f49bab75 100644 --- a/Workflow/Tests/AnyWorkflowTests.swift +++ b/Workflow/Tests/AnyWorkflowTests.swift @@ -54,6 +54,12 @@ public class AnyWorkflowTests: XCTestCase { } wait(for: [renderingExpectation, outputExpectation], timeout: 1) } + + func testBaseValue() { + let erased = OnOutputWorkflow().asAnyWorkflow() + + XCTAssertNotNil(erased.base as? OnOutputWorkflow) + } } /// Has no state or output, simply renders a reversed string From fae80e669e06c30e8aa25978b03bb9041de4cf7a Mon Sep 17 00:00:00 2001 From: jamieQ Date: Tue, 7 Mar 2023 11:02:36 -0600 Subject: [PATCH 6/6] more tests --- Workflow/Tests/AnyWorkflowTests.swift | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Workflow/Tests/AnyWorkflowTests.swift b/Workflow/Tests/AnyWorkflowTests.swift index 2f49bab75..e72a22f43 100644 --- a/Workflow/Tests/AnyWorkflowTests.swift +++ b/Workflow/Tests/AnyWorkflowTests.swift @@ -55,6 +55,28 @@ 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()