diff --git a/Sources/SwiftCurrent/Models/FlowRepresentableMetadata.swift b/Sources/SwiftCurrent/Models/FlowRepresentableMetadata.swift index 01bcca4f6..b86e50622 100644 --- a/Sources/SwiftCurrent/Models/FlowRepresentableMetadata.swift +++ b/Sources/SwiftCurrent/Models/FlowRepresentableMetadata.swift @@ -14,7 +14,7 @@ import Foundation ### Discussion Every time a `Workflow` is created, the defining characteristics about a `FlowRepresentable` are stored in the `FlowRepresentableMetadata` to be used later. */ -public class FlowRepresentableMetadata { +open class FlowRepresentableMetadata { /// Preferred `LaunchStyle` of the associated `FlowRepresentable`. public private(set) var launchStyle: LaunchStyle /// Preferred `FlowPersistence` of the associated `FlowRepresentable`; set when `FlowRepresentableMetadata` instantiates an instance. diff --git a/Sources/SwiftCurrent/TypeErased/AnyWorkflow.swift b/Sources/SwiftCurrent/TypeErased/AnyWorkflow.swift index eede95ba4..ea6f49eae 100644 --- a/Sources/SwiftCurrent/TypeErased/AnyWorkflow.swift +++ b/Sources/SwiftCurrent/TypeErased/AnyWorkflow.swift @@ -34,6 +34,9 @@ public class AnyWorkflow { /// The first `LinkedList.Node` of the wrapped `Workflow`. public var first: Element? { storageBase.first } + /// The last `LinkedList.Node` of the wrapped `Workflow`. + public var last: Element? { storageBase.last } + fileprivate var storageBase: AnyWorkflowStorageBase /// Creates a type erased `Workflow`. @@ -111,6 +114,7 @@ fileprivate class AnyWorkflowStorageBase { var orchestrationResponder: OrchestrationResponder? var count: Int { fatalError("count not overridden by AnyWorkflowStorage") } var first: LinkedList<_WorkflowItem>.Element? { fatalError("first not overridden by AnyWorkflowStorage") } + var last: LinkedList<_WorkflowItem>.Element? { fatalError("last not overridden by AnyWorkflowStorage") } // https://github.com/wwt/SwiftCurrent/blob/main/.github/STYLEGUIDE.md#type-erasure // swiftlint:disable:next unavailable_function @@ -157,6 +161,7 @@ fileprivate final class AnyWorkflowStorage: AnyWorkflowSto override var count: Int { workflow.count } override var first: LinkedList<_WorkflowItem>.Element? { workflow.first } + override var last: LinkedList<_WorkflowItem>.Element? { workflow.last } init(_ workflow: Workflow) { self.workflow = workflow diff --git a/Sources/SwiftCurrent_SwiftUI/Extensions/WorkflowExtensions.swift b/Sources/SwiftCurrent_SwiftUI/Extensions/WorkflowExtensions.swift index 347fb4506..7f5e41055 100644 --- a/Sources/SwiftCurrent_SwiftUI/Extensions/WorkflowExtensions.swift +++ b/Sources/SwiftCurrent_SwiftUI/Extensions/WorkflowExtensions.swift @@ -11,8 +11,248 @@ import SwiftUI @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) extension Workflow where F: FlowRepresentable & View { + /* + TODO: The below initializers are left public to facilitate testing data-driven workflows. + They should be made private when data driven progress allows. + */ + + /** + Creates a `Workflow` with a `FlowRepresentable`. + - Parameter type: a reference to the first `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a `FlowPersistence` representing how this item in the workflow should persist. + */ + public convenience init(_ type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping @autoclosure () -> FlowPersistence = .default) { + self.init(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + } + + /** + Creates a `Workflow` with a `FlowRepresentable`. + - Parameter type: a reference to the first `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a `FlowPersistence` representing how this item in the workflow should persist. + */ + public convenience init(_ type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping (F.WorkflowInput) -> FlowPersistence) { + self.init(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { data in + guard case.args(let extracted) = data, + let cast = extracted as? F.WorkflowInput else { return .default } + + return flowPersistence(cast) + }) + } + + /** + Creates a `Workflow` with a `FlowRepresentable`. + - Parameter type: a reference to the first `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + */ + public convenience init(_ type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping () -> FlowPersistence) where F.WorkflowInput == Never { + self.init(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + } + /// Called when the workflow should be terminated, and the app should return to the point before the workflow was launched. public func abandon() { AnyWorkflow(self).abandon() } + + // TODO: Remove the following untested functions when data-driven is more mature + + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: FR.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping (FR.WorkflowInput) -> FlowPersistence) -> Workflow where F.WorkflowOutput == FR.WorkflowInput { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { data in + guard case.args(let extracted) = data, + let cast = extracted as? FR.WorkflowInput else { return .default } + + return flowPersistence(cast) + }) + return workflow + } + + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping (F.WorkflowInput) -> FlowPersistence) -> Workflow where F.WorkflowInput == AnyWorkflow.PassedArgs { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { data in + guard case.args(let extracted) = data, + let cast = extracted as? F.WorkflowInput else { return .default } + + return flowPersistence(cast) + }) + return workflow + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension Workflow where F: FlowRepresentable & View, F.WorkflowOutput == Never { + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping () -> FlowPersistence) -> Workflow where F.WorkflowInput == AnyWorkflow.PassedArgs { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + return workflow + } + + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping () -> FlowPersistence) -> Workflow where F.WorkflowInput == Never { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + return workflow + } + + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: FR.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping @autoclosure () -> FlowPersistence = .default) -> Workflow where FR.WorkflowInput == AnyWorkflow.PassedArgs { + let wf = Workflow(first) + wf.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + return wf + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension Workflow where F: FlowRepresentable & View, F.WorkflowOutput == AnyWorkflow.PassedArgs { + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping () -> FlowPersistence) -> Workflow where F.WorkflowInput == AnyWorkflow.PassedArgs { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + return workflow + } + + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping (F.WorkflowInput) -> FlowPersistence) -> Workflow where F.WorkflowInput == AnyWorkflow.PassedArgs { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { data in + guard case.args(let extracted) = data, + let cast = extracted as? F.WorkflowInput else { return .default } + + return flowPersistence(cast) + }) + return workflow + } + + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping () -> FlowPersistence) -> Workflow where F.WorkflowInput == Never { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + return workflow + } + + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a closure returning a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: F.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping (F.WorkflowInput) -> FlowPersistence) -> Workflow { + let workflow = Workflow(first) + workflow.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { data in + guard case.args(let extracted) = data, + let cast = extracted as? F.WorkflowInput else { return .default } + + return flowPersistence(cast) + }) + return workflow + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension Workflow where F.WorkflowOutput == Never { + /** + Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the `FlowRepresentable.WorkflowInput` of this item. + - Parameter type: a reference to the next `FlowRepresentable`'s concrete type in the workflow. + - Parameter launchStyle: the `LaunchStyle` the `FlowRepresentable` should use while it's part of this workflow. + - Parameter flowPersistence: a `FlowPersistence` representing how this item in the workflow should persist. + - Returns: a new workflow with the additional `FlowRepresentable` item. + */ + public func thenProceed(with type: FR.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping @autoclosure () -> FlowPersistence = .default) -> Workflow where FR.WorkflowInput == Never { + let wf = Workflow(first) + wf.append(ExtendedFlowRepresentableMetadata(flowRepresentableType: type, + launchStyle: launchStyle) { _ in flowPersistence() }) + return wf + } } diff --git a/Sources/SwiftCurrent_SwiftUI/Models/ExtendedFlowRepresentableMetadata.swift b/Sources/SwiftCurrent_SwiftUI/Models/ExtendedFlowRepresentableMetadata.swift new file mode 100644 index 000000000..760ba21b0 --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Models/ExtendedFlowRepresentableMetadata.swift @@ -0,0 +1,40 @@ +// +// ExtendedFlowRepresentableMetadata.swift +// SwiftCurrent +// +// Created by Morgan Zellers on 11/2/21. +// Copyright © 2021 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +class ExtendedFlowRepresentableMetadata: FlowRepresentableMetadata { + private(set) var workflowItemFactory: (AnyWorkflowItem?) -> AnyWorkflowItem + + init(flowRepresentableType: FR.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping (AnyWorkflow.PassedArgs) -> FlowPersistence, + flowRepresentableFactory: @escaping (AnyWorkflow.PassedArgs) -> AnyFlowRepresentable) { + workflowItemFactory = { + guard let wrappedWorkflowItem = $0 else { return AnyWorkflowItem(view: WorkflowItem(FR.self)) } + return AnyWorkflowItem(view: WorkflowItem(FR.self) { wrappedWorkflowItem }) + } + + super.init(flowRepresentableType, launchStyle: launchStyle, flowPersistence: flowPersistence, flowRepresentableFactory: flowRepresentableFactory) + } + + init(flowRepresentableType: FR.Type, + launchStyle: LaunchStyle = .default, + flowPersistence: @escaping (AnyWorkflow.PassedArgs) -> FlowPersistence) { + workflowItemFactory = { + guard let wrappedWorkflowItem = $0 else { return AnyWorkflowItem(view: WorkflowItem(FR.self)) } + return AnyWorkflowItem(view: WorkflowItem(FR.self) { wrappedWorkflowItem }) + } + + super.init(flowRepresentableType, launchStyle: launchStyle, flowPersistence: flowPersistence) { args in + AnyFlowRepresentableView(type: FR.self, args: args) + } + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/TypeErased/AnyWorkflowItem.swift b/Sources/SwiftCurrent_SwiftUI/TypeErased/AnyWorkflowItem.swift new file mode 100644 index 000000000..3211c9bc4 --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/TypeErased/AnyWorkflowItem.swift @@ -0,0 +1,23 @@ +// +// AnyWorkflowItem.swift +// SwiftCurrent +// +// Created by Morgan Zellers on 11/2/21. +// Copyright © 2021 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public struct AnyWorkflowItem: View { + let inspection = Inspection() + private let _body: AnyView + + public var body: some View { + _body.onReceive(inspection.notice) { inspection.visit(self, $0) } + } + + init(view: WorkflowItem) { + _body = AnyView(view) + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift index e18179899..3a04f63bf 100644 --- a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift +++ b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift @@ -89,26 +89,26 @@ public struct WorkflowItem Wrapped) where Content == F { - let metadata = FlowRepresentableMetadata(Content.self, - launchStyle: .new, - flowPersistence: flowPersistenceClosure, - flowRepresentableFactory: factory) + let metadata = ExtendedFlowRepresentableMetadata(flowRepresentableType: Content.self, + launchStyle: .new, + flowPersistence: flowPersistenceClosure, + flowRepresentableFactory: factory) _metadata = State(initialValue: metadata) _wrapped = State(initialValue: wrapped()) } diff --git a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift index e01b40a95..378685165 100644 --- a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift +++ b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift @@ -77,6 +77,38 @@ public struct WorkflowLauncher: View { .onReceive(inspection.notice) { inspection.visit(self, $0) } } + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter workflow: workflow to be launched; must contain `FlowRepresentable`s of type `View` + */ + public init(isLaunched: Binding, workflow: Workflow) where Content == AnyWorkflowItem { + self.init(isLaunched: isLaunched, startingArgs: .none, workflow: AnyWorkflow(workflow)) + } + + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter startingArgs: arguments passed to the first loaded `FlowRepresentable` in the underlying `Workflow`. + - Parameter workflow: workflow to be launched; must contain `FlowRepresentable`s of type `View` + */ + public init(isLaunched: Binding, startingArgs: AnyWorkflow.PassedArgs, workflow: Workflow) where Content == AnyWorkflowItem { + self.init(isLaunched: isLaunched, startingArgs: startingArgs, workflow: AnyWorkflow(workflow)) + } + + private init(isLaunched: Binding, startingArgs: AnyWorkflow.PassedArgs, workflow: AnyWorkflow) where Content == AnyWorkflowItem { + workflow.forEach { + assert($0.value.metadata is ExtendedFlowRepresentableMetadata) + } + _isLaunched = isLaunched + let model = WorkflowViewModel(isLaunched: isLaunched, launchArgs: startingArgs) + _model = StateObject(wrappedValue: model) + _launcher = StateObject(wrappedValue: Launcher(workflow: workflow, + responder: model, + launchArgs: startingArgs)) + _content = State(wrappedValue: WorkflowLauncher.itemToLaunch(from: workflow)) + } + /** Creates a base for proceeding with a `WorkflowItem`. - Parameter isLaunched: binding that controls launching the underlying `Workflow`. @@ -167,6 +199,26 @@ public struct WorkflowLauncher: View { onFinish.forEach { $0(args) } } + private static func itemToLaunch(from workflow: AnyWorkflow) -> AnyWorkflowItem { + let lastMetadata = workflow.last?.value.metadata as? ExtendedFlowRepresentableMetadata + let lastItem = lastMetadata?.workflowItemFactory(nil) + + if let headItem = WorkflowLauncher.findHeadItem(element: workflow.last, item: lastItem) { + return headItem + } else if let lastItem = lastItem { + return lastItem + } + + fatalError("Workflow has no items to launch") + } + + private static func findHeadItem(element: AnyWorkflow.Element?, item: AnyWorkflowItem?) -> AnyWorkflowItem? { + guard let previous = element?.previous, + let previousItem = (previous.value.metadata as? ExtendedFlowRepresentableMetadata)?.workflowItemFactory(item) else { return item } + + return findHeadItem(element: previous, item: previousItem) + } + /// Adds an action to perform when this `Workflow` has finished. public func onFinish(closure: @escaping (AnyWorkflow.PassedArgs) -> Void) -> Self { var onFinish = self.onFinish diff --git a/Tests/SwiftCurrentTests/FlowRepresentableMetadataConsumerTests.swift b/Tests/SwiftCurrentTests/FlowRepresentableMetadataConsumerTests.swift new file mode 100644 index 000000000..e9de32cfb --- /dev/null +++ b/Tests/SwiftCurrentTests/FlowRepresentableMetadataConsumerTests.swift @@ -0,0 +1,42 @@ +// +// FlowRepresentableMetadataConsumerTests.swift +// SwiftCurrent +// +// Created by Morgan Zellers on 11/2/21. +// Copyright © 2021 WWT and Tyler Thompson. All rights reserved. +// + +import XCTest +import SwiftCurrent + +class FlowRepresentableMetadataConsumerTests: XCTestCase { + func testOverridingFlowRepresentbleMetadata() { + class SpecialConformanceClass { } + + class NewMetadata: FlowRepresentableMetadata { + var wf: String? // AnyWFItem + + private override init(_ flowRepresentableType: FR.Type, launchStyle: LaunchStyle = .default, flowPersistence: @escaping (AnyWorkflow.PassedArgs) -> FlowPersistence, flowRepresentableFactory: @escaping (AnyWorkflow.PassedArgs) -> AnyFlowRepresentable) where FR : FlowRepresentable { + + + super.init(flowRepresentableType, launchStyle: launchStyle, flowPersistence: flowPersistence, flowRepresentableFactory: flowRepresentableFactory) + } + + convenience init(flowRepresentableType: FR.Type, launchStyle: LaunchStyle = .default, flowPersistence: @escaping (AnyWorkflow.PassedArgs) -> FlowPersistence, flowRepresentableFactory: @escaping (AnyWorkflow.PassedArgs) -> AnyFlowRepresentable) { + + self.init(flowRepresentableType, launchStyle: launchStyle, flowPersistence: flowPersistence, flowRepresentableFactory: flowRepresentableFactory) + + wf = String(describing: flowRepresentableType) + } + } + + final class FR1: SpecialConformanceClass, FlowRepresentable { + var _workflowPointer: AnyFlowRepresentable? + } + + let _ = NewMetadata(flowRepresentableType: FR1.self, + flowPersistence: { _ in .default }) { _ in + AnyFlowRepresentable(FR1.self, args: .none) + } + } +} diff --git a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift index e8f8d57e3..72430aa54 100644 --- a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift +++ b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift @@ -731,6 +731,258 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { wait(for: [exp], timeout: TestConstant.timeout) } + + func testLaunchingAWorkflowWithOneItemFromAnAnyWorkflow() { + struct FR1: View, FlowRepresentable, Inspectable { + weak var _workflowPointer: AnyFlowRepresentable? + + var body: some View { + Button("Proceed") { proceedInWorkflow() } + } + } + + let wf = Workflow(FR1.self) + + let launcher = WorkflowLauncher(isLaunched: .constant(true), workflow: wf) + + let exp = ViewHosting.loadView(launcher).inspection.inspect { view in + XCTAssertNoThrow(try view.find(FR1.self)) + } + + wait(for: [exp], timeout: TestConstant.timeout) + } + + func testLaunchingAMultiTypeLongWorkflowFromAnAnyWorkflow() { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = AnyWorkflow.PassedArgs + var _workflowPointer: AnyFlowRepresentable? + private let data: AnyWorkflow.PassedArgs + var body: some View { Text("FR2 type") } + + init(with data: AnyWorkflow.PassedArgs) { + self.data = data + } + } +// struct FR3: View, FlowRepresentable, Inspectable { +// let str: String +// init(with str: String) { +// self.str = str +// } +// var _workflowPointer: AnyFlowRepresentable? +// var body: some View { Text("FR3 type, \(str)") } +// } +// struct FR4: View, FlowRepresentable, Inspectable { +// var _workflowPointer: AnyFlowRepresentable? +// var body: some View { Text("FR4 type") } +// } + + let expectOnFinish = expectation(description: "OnFinish called") + let expectedArgs = UUID().uuidString + + let wf = Workflow(FR1.self) + .thenProceed(with: FR2.self) { .default } + + let expectViewLoaded = ViewHosting.loadView( + WorkflowLauncher(isLaunched: .constant(true), workflow: wf) + .onFinish { _ in + expectOnFinish.fulfill() + }).inspection.inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow()) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR2.self).text().string(), "FR2 type") + XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow(.args(expectedArgs))) + } + } + + wait(for: [expectOnFinish, expectViewLoaded], timeout: TestConstant.timeout) + } + + func testLaunchingAWorkflowFromAnAnyWorkflow() { + struct FR1: View, FlowRepresentable, Inspectable { + weak var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, PassthroughFlowRepresentable, Inspectable { + weak var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + weak var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + + let wf = Workflow(FR1.self) + .thenProceed(with: FR2.self, flowPersistence: { .default }) + .thenProceed(with: FR3.self, flowPersistence: { .default }) + + let launcher = WorkflowLauncher(isLaunched: .constant(true), workflow: wf) + let expectOnFinish = expectation(description: "OnFinish called") + + let expectViewLoaded = ViewHosting.loadView( + launcher + .onFinish { _ in + expectOnFinish.fulfill() + }).inspection.inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow()) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR2.self).text().string(), "FR2 type") + XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow()) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR3.self).text().string(), "FR3 type") + XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow()) + } + } + } + + wait(for: [expectOnFinish, expectViewLoaded], timeout: TestConstant.timeout) + } + + func testWorkflowLaunchedFromAnAnyWorkflowCanHavePassthroughFlowRepresentableInTheMiddle() { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = String + var _workflowPointer: AnyFlowRepresentable? + private let data: AnyWorkflow.PassedArgs + var body: some View { Text("FR2 type") } + + init(with args: AnyWorkflow.PassedArgs) { + self.data = args + } + } + struct FR3: View, FlowRepresentable, Inspectable { + typealias WorkflowInput = String + let str: String + init(with str: String) { + self.str = str + } + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type, \(str)") } + } + struct FR4: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR4 type") } + } + + let wf = Workflow(FR1.self) + .thenProceed(with: FR2.self, flowPersistence: { .default }) + .thenProceed(with: FR3.self, flowPersistence: { _ in .default }) + .thenProceed(with: FR4.self, flowPersistence: { .default }) + + let launcher = WorkflowLauncher(isLaunched: .constant(true), workflow: wf) + let expectOnFinish = expectation(description: "OnFinish called") + let expectedArgs = UUID().uuidString + + let expectViewLoaded = ViewHosting.loadView( + launcher + .onFinish { _ in + expectOnFinish.fulfill() + }).inspection.inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow()) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR2.self).text().string(), "FR2 type") + XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow(expectedArgs)) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR3.self).text().string(), "FR3 type, \(expectedArgs)") + XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow()) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR4.self).text().string(), "FR4 type") + XCTAssertNoThrow(try viewUnderTest.find(FR4.self).actualView().proceedInWorkflow()) + } + } + } + } + + wait(for: [expectOnFinish, expectViewLoaded], timeout: TestConstant.timeout) + } + + func testWorkflowLaunchedFromAnAnyWorkflowCanHaveStartingArgs() { + struct FR1: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = AnyWorkflow.PassedArgs + var _workflowPointer: AnyFlowRepresentable? + var args: AnyWorkflow.PassedArgs + var body: some View { Text("FR1 type, \(args.extractArgs(defaultValue: "") as! String)") } + + init(with args: AnyWorkflow.PassedArgs) { + self.args = args + } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var args: AnyWorkflow.PassedArgs + var body: some View { Text("FR2 type, \(args.extractArgs(defaultValue: "") as! String)") } + + init(with args: AnyWorkflow.PassedArgs) { + self.args = args + } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + struct FR4: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR4 type") } + } + + let wf = Workflow(FR1.self) + .thenProceed(with: FR2.self, flowPersistence: { _ in .default }) + .thenProceed(with: FR3.self, flowPersistence: { _ in .default }) + .thenProceed(with: FR4.self, flowPersistence: { .default }) + + let expectedArgs = UUID().uuidString + let launcher = WorkflowLauncher(isLaunched: .constant(true), startingArgs: .args(expectedArgs), workflow: wf) + let expectOnFinish = expectation(description: "OnFinish called") + + let expectViewLoaded = ViewHosting.loadView( + launcher + .onFinish { _ in + expectOnFinish.fulfill() + }).inspection.inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type, \(expectedArgs)") + XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow(.args(expectedArgs))) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR2.self).text().string(), "FR2 type, \(expectedArgs)") + XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow()) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR3.self).text().string(), "FR3 type") + XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow()) + try viewUnderTest.actualView().inspect { viewUnderTest in + XCTAssertEqual(try viewUnderTest.find(FR4.self).text().string(), "FR4 type") + XCTAssertNoThrow(try viewUnderTest.find(FR4.self).actualView().proceedInWorkflow()) + } + } + } + } + + wait(for: [expectOnFinish, expectViewLoaded], timeout: TestConstant.timeout) + } + + func testIfNoWorkflowItemsThenFatalError() throws { + struct FR1: View, FlowRepresentable, Inspectable { + weak var _workflowPointer: AnyFlowRepresentable? + + var body: some View { + Button("Proceed") { proceedInWorkflow() } + } + } + + let wf = Workflow(FR1.self) + wf.removeLast() + + try XCTAssertThrowsFatalError { + _ = WorkflowLauncher(isLaunched: .constant(true), workflow: wf) + } + } } @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) diff --git a/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift b/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift index e14f62d80..ad16caa78 100644 --- a/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift +++ b/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift @@ -15,6 +15,8 @@ import SwiftUI @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) extension WorkflowItem: Inspectable { } @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension AnyWorkflowItem: Inspectable { } +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) extension WorkflowLauncher: Inspectable { } @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) extension ViewControllerWrapper: Inspectable { }