diff --git a/.github/.jazzy.yaml b/.github/.jazzy.yaml index 2a9cb9502..3af201432 100644 --- a/.github/.jazzy.yaml +++ b/.github/.jazzy.yaml @@ -27,11 +27,9 @@ custom_categories: - Working with Modals - name: Creating Workflows in SwiftUI children: - - WorkflowLauncher + - WorkflowView - WorkflowItem - - View - - App - - Scene + - WorkflowBuilder - name: How to use SwiftCurrent with UIKit children: - Using Programmatic Views diff --git a/.github/UPGRADE_PATH.md b/.github/UPGRADE_PATH.md index e292e8daf..7130bdd23 100644 --- a/.github/UPGRADE_PATH.md +++ b/.github/UPGRADE_PATH.md @@ -3,6 +3,26 @@ Use this document to help you understand how to update between major versions of Our directions are written for only 1 major version upgrade at a time, as we have found that to be the best experience. +
+ V4 -> V5 + + ## SwiftUI - WorkflowView + Our approach to a SwiftUI API drastically changed. This new API is much more idiomatic and natural feeling when using SwiftUI. Additionally, it enables a series of new features. Previously, you used `thenProceed(with:)` and `WorkflowLauncher` to launch a workflow in SwiftUI. You now use `WorkflowGroup` and `WorkflowItem`. + + ```swift + WorkflowView { + WorkflowItem(FirstView.self) // This view is shown first + WorkflowItem(SecondView.self) // After proceeding, this view is shown + } + ``` + + To transition from the old API, replace your calls to `WorkflowLauncher` with `WorkflowView`. Also note that `startingArgs` has changed to `launchingWith`. So the full signature changes from `WorkflowLauncher(isLaunched: .constant(true), startingArgs: "someArgs")` to `WorkflowView(isLaunched: .constant(true), launchingWith: "someArgs")`. + + `WorkflowView`'s initializer defaults `isLaunched` to `.constant(true)` meaning you can exclude that parameter and just use `WorkflowView(launchingWith: "someArgs")` +
+ +--- +
V3 -> V4 diff --git a/.github/abstract/Controlling Presentation.md b/.github/abstract/Controlling Presentation.md index d530bcc37..0c668d254 100644 --- a/.github/abstract/Controlling Presentation.md +++ b/.github/abstract/Controlling Presentation.md @@ -4,7 +4,7 @@ SwiftCurrent allows you to control how your workflow presents its `FlowRepresent In UIKit, you control presentation with `LaunchStyle.PresentationType`. The default is a contextual presentation mode. If it detects you are in a navigation view, it'll present by pushing onto the navigation stack. If it cannot detect a navigation view, it presents modally. Alternatively, you can explicitly state you'd like it to present modally or in a navigation stack when you define your `Workflow`. ### In SwiftUI -In SwiftUI, you control presentation using `LaunchStyle.SwiftUI.PresentationType`. The default is simple view replacement. This is especially powerful because your workflows in SwiftUI do not need to be an entire screen; they can be just part of a view. Using the default presentation type, you can also get fine-grained control over animations. You can also explicitly state you'd like it to present modally (using a sheet or fullScreenCover) or in a navigation stack when you define your `WorkflowLauncher`. +In SwiftUI, you control presentation using `LaunchStyle.SwiftUI.PresentationType`. The default is simple view replacement. This is especially powerful because your workflows in SwiftUI do not need to be an entire screen; they can be just part of a view. Using the default presentation type, you can also get fine-grained control over animations. You can also explicitly state you'd like it to present modally (using a sheet or fullScreenCover) or in a navigation stack when you define your `WorkflowView`. ### Persistence You can control what happens to items in your workflow using `FlowPersistence`. Using `FlowPersistence.persistWhenSkipped` means that when `FlowRepresentable.shouldLoad` returns false, the item is still stored on the workflow. If, for example, you're in a navigation stack, this means the item *is* skipped, but you can back up to it. diff --git a/.github/abstract/Creating Workflows in SwiftUI.md b/.github/abstract/Creating Workflows in SwiftUI.md index a947ed5ea..984b17294 100644 --- a/.github/abstract/Creating Workflows in SwiftUI.md +++ b/.github/abstract/Creating Workflows in SwiftUI.md @@ -22,15 +22,18 @@ struct FirstView: View, FlowRepresentable { > **Note:** `FlowRepresentable.proceedInWorkflow()` is what you call to have your view move forward to the next item in the `Workflow` it is part of. ### Step 2: -Define your `WorkflowLauncher`. This indicates if the workflow is shown and describes what items are in it. +Define your `WorkflowView`. This indicates if the workflow is shown and describes what items are in it. #### Example: ```swift -WorkflowLauncher(isLaunched: .constant(true)) { // Could also have been $someStateOrBindingBoolean - thenProceed(with: FirstView.self) { // thenProceed is a function to create a `WorkflowItem` - thenProceed(with: SecondView.self) { // Use closures to define what comes next - thenProceed(with: ThirdView.self) // The final item needs no closures - } - } +/* + Each item in the workflow is defined as a `WorkflowItem` + passing the type of the FlowRepresentable to create + when appropriate as the workflow proceeds +*/ +WorkflowView { + WorkflowItem(FirstView.self) + WorkflowItem(SecondView.self) + WorkflowItem(ThirdView.self) } ``` diff --git a/.github/abstract/Creating Workflows.md b/.github/abstract/Creating Workflows.md index b9d0a267e..2e8bb429e 100644 --- a/.github/abstract/Creating Workflows.md +++ b/.github/abstract/Creating Workflows.md @@ -9,4 +9,4 @@ Workflows enforce (either at compile-time or run-time) that the sequence of `Flo In some cases, like UIKit, the compiler is efficient enough to give you compile-time feedback if a workflow is malformed. This means that run-time errors are rare. They can still occur; for example, if you have Item1 declare a `FlowRepresentable.WorkflowOutput` of `AnyWorkflow.PassedArgs`, then call `FlowRepresentable.proceedInWorkflow(_:)` with `.args("string")`, but Item2 has a `FlowRepresentable.WorkflowInput` of `Int`, there'll be a run-time error because the data passed forward does not meet expectations. -In SwiftUI, the compiler was not efficient enough to give the same compile-time feedback on malformed workflows. When that safety was added, the compiler only allowed for small workflows to be created. To combat this, SwiftUI is heavily run-time influenced. When you create a `WorkflowLauncher`, the launcher performs a run-time check to guarantee the workflow is well-formed. This means that if you wanted to test your workflow was well-formed, all you have to do is instantiate a `WorkflowLauncher`. +In SwiftUI, the compiler was not efficient enough to give the same compile-time feedback on malformed workflows. When that safety was added, the compiler only allowed for small workflows to be created. To combat this, SwiftUI is heavily run-time influenced. When you create a `WorkflowView`, the library performs a run-time check to guarantee the workflow is well-formed. This means that if you wanted to test your workflow was well-formed, all you need to do is instantiate a `WorkflowView`. diff --git a/.github/fastlane/Fastfile b/.github/fastlane/Fastfile index 478d6d50b..d7dee64cb 100644 --- a/.github/fastlane/Fastfile +++ b/.github/fastlane/Fastfile @@ -68,13 +68,13 @@ platform :ios do lane :cocoapods_liblint do pod_lib_lint( - podspec: '../SwiftCurrent.podspec', + podspec: '../SwiftCurrent.podspec', allow_warnings: true, no_clean: true ) end - lane :lint do + lane :lint do swiftlint( config_file: 'SwiftCurrentLint/.swiftlint.yml', raise_if_swiftlint_error: true, @@ -82,8 +82,8 @@ platform :ios do ) end - lane :lintfix do - sh('swiftlint --fix --config=../../SwiftCurrentLint/.swiftlint.yml') + lane :lintfix do + sh('swiftlint --fix --config=../SwiftCurrentLint/.swiftlint.yml') end desc "Release a new version with a patch bump_type" diff --git a/.github/guides/Getting Started with SwiftUI.md b/.github/guides/Getting Started with SwiftUI.md index ef01dfd94..20b837626 100644 --- a/.github/guides/Getting Started with SwiftUI.md +++ b/.github/guides/Getting Started with SwiftUI.md @@ -2,7 +2,7 @@ This guide will walk you through getting a `Workflow` up and running in a new iOS project. If you would like to see an existing project, clone the repo and view the `SwiftUIExample` scheme in `SwiftCurrent.xcworkspace`. -The app in this guide is going to be very simple. It consists of a view that will host the `WorkflowLauncher`, a view to enter an email address, and an optional view for when the user enters an email with `@wwt.com` in it. Here is a preview of what the app will look like: +The app in this guide is going to be very simple. It consists of a view that will host the `WorkflowView`, a view to enter an email address, and an optional view for when the user enters an email with `@wwt.com` in it. Here is a preview of what the app will look like: ![Preview image of app](https://user-images.githubusercontent.com/79471462/131556533-f2ad1e6c-9acd-4d62-94ac-9140c9718f95.gif) @@ -111,7 +111,7 @@ struct SecondView_Previews: PreviewProvider { ## Launching the `Workflow` -Next we add a `WorkflowLauncher` to the body of our starting app view, in this case `ContentView`. +Next we add a `WorkflowView` to the body of our starting app view, in this case `ContentView`. ```swift import SwiftUI @@ -123,10 +123,11 @@ struct ContentView: View { if !workflowIsPresented { Button("Present") { workflowIsPresented = true } } else { - WorkflowLauncher(isLaunched: $workflowIsPresented, startingArgs: "SwiftCurrent") { // SwiftCurrent - thenProceed(with: FirstView.self) { // SwiftCurrent - thenProceed(with: SecondView.self).applyModifiers { $0.padding().border(Color.gray) } // SwiftCurrent - }.applyModifiers { firstView in firstView.padding().border(Color.gray) } // SwiftCurrent + WorkflowView(isLaunched: $workflowIsPresented, launchingWith: "SwiftCurrent") { // SwiftCurrent + WorkflowItem(FirstView.self) // SwiftCurrent + .applyModifiers { firstView in firstView.padding().border(Color.gray) } // SwiftCurrent + WorkflowItem(SecondView.self) // SwiftCurrent + .applyModifiers { $0.padding().border(Color.gray) } // SwiftCurrent }.onFinish { passedArgs in // SwiftCurrent workflowIsPresented = false guard case .args(let emailAddress as String) = passedArgs else { @@ -152,21 +153,21 @@ struct Content_Previews: PreviewProvider {
-In SwiftUI, the Workflow type is handled by the library when you start with a WorkflowLauncher. +In SwiftUI, the Workflow type is handled by the library when you start with a WorkflowView.
#### **Where is the type safety I heard about?**
-WorkflowLauncher is specialized with your startingArgs type. FlowRepresentable is specialized with the FlowRepresentable.WorkflowInput and FlowRepresentable.WorkflowOutput associated types. These all work together when creating your flow at run-time to ensure the validity of your Workflow. If the output of FirstView does not match the input of SecondView, the library will send an error when creating the Workflow. +WorkflowView is specialized with your launchingWith type. FlowRepresentable is specialized with the FlowRepresentable.WorkflowInput and FlowRepresentable.WorkflowOutput associated types. These all work together when creating your flow at run-time to ensure the validity of your Workflow. If the output of FirstView does not match the input of SecondView, the library will send an error when creating the Workflow.
-#### **What's going on with this `startingArgs` and `passedArgs`?** +#### **What's going on with this `launchingWith` and `passedArgs`?**
-startingArgs are the AnyWorkflow.PassedArgs handed to the first FlowRepresentable in the workflow. These arguments are used to pass data and determine if the view should load. +launchingWith are the AnyWorkflow.PassedArgs handed to the first FlowRepresentable in the workflow. These arguments are used to pass data and determine if the view should load. passedArgs are the AnyWorkflow.PassedArgs coming from the last view in the workflow. onFinish is only called when the user has gone through all the screens in the Workflow by navigation or skipping. For this workflow, passedArgs is going to be the output of FirstView or SecondView, depending on the email signature typed in FirstView. To extract the value, we unwrap the variable within the case of .args() as we expect this workflow to return some argument.
@@ -205,9 +206,8 @@ final class FirstViewController: UIWorkflowItem, FlowRepresentable Now in SwiftUI simply reference that controller. ```swift -WorkflowLauncher(isLaunched: $workflowIsPresented) { // SwiftCurrent - thenProceed(with: FirstViewController.self) { // SwiftCurrent - thenProceed(with: SecondView.self) // SwiftCurrent - } +WorkflowView(isLaunched: $workflowIsPresented) { // SwiftCurrent + WorkflowItem(FirstViewController.self) // SwiftCurrent + WorkflowItem(SecondView.self) // SwiftCurrent } ``` diff --git a/.github/guides/Working with Modals.md b/.github/guides/Working with Modals.md index 4cd065464..b734ffe7c 100644 --- a/.github/guides/Working with Modals.md +++ b/.github/guides/Working with Modals.md @@ -3,12 +3,9 @@ When constructing a workflow, you can use `WorkflowItem.presentationType(_:)` al #### Example ```swift -NavigationView { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FirstView.self) { - thenProceed(with: SecondView.self).presentationType(.modal) - } - } +WorkflowView { + WorkflowItem(FirstView.self) + WorkflowItem(SecondView.self).presentationType(.modal) } ``` @@ -22,11 +19,8 @@ When you use a presentation type of `LaunchStyle.SwiftUI.PresentationType.modal` #### Example The following will use a full-screen cover: ```swift -NavigationView { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FirstView.self) { - thenProceed(with: SecondView.self).presentationType(.modal(.fullScreenCover)) - } - } +WorkflowView { + WorkflowItem(FirstView.self) + WorkflowItem(SecondView.self).presentationType(.modal(.fullScreenCover)) } ``` diff --git a/.github/guides/Working with NavigationView.md b/.github/guides/Working with NavigationView.md index 423b3c1eb..f0246ab42 100644 --- a/.github/guides/Working with NavigationView.md +++ b/.github/guides/Working with NavigationView.md @@ -4,10 +4,10 @@ When constructing a workflow, you can use `WorkflowItem.presentationType(_:)` al #### Example ```swift NavigationView { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FirstView.self) { - thenProceed(with: SecondView.self) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FirstView.self) + .presentationType(.navigationLink) + WorkflowItem(SecondView.self) } } ``` @@ -17,15 +17,15 @@ With that, you've described that `FirstView` should be wrapped in a `NavigationL > **NOTE:** The `NavigationLink` is in the background of the view to prevent your entire view from being tappable. ### Different NavigationView Styles -SwiftCurrent comes with a convenience function on `WorkflowLauncher` that tries to pick the best `NavigationViewStyle` for a `Workflow`. Normally that's stack-based navigation. +SwiftCurrent comes with a convenience function on `WorkflowView` that tries to pick the best `NavigationViewStyle` for a `Workflow`. Normally that's stack-based navigation. #### Example The earlier example could be rewritten as: ```swift -WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FirstView.self) { - thenProceed(with: SecondView.self) - }.presentationType(.navigationLink) +WorkflowView { + WorkflowItem(FirstView.self) + .presentationType(.navigationLink) + WorkflowItem(SecondView.self) }.embedInNavigationView() ``` @@ -36,10 +36,10 @@ If you want to use column-based navigation you can simply manage it yourself: ```swift NavigationView { FirstColumn() // Could ALSO be a workflow - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FirstView.self) { - thenProceed(with: SecondView.self) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FirstView.self) + .presentationType(.navigationLink) + WorkflowItem(SecondView.self) } // don't call embedInNavigationView here } ``` diff --git a/ExampleApps/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj b/ExampleApps/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj index a412e0c39..2988d1589 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj +++ b/ExampleApps/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 628952DA27E5281700FDDCEF /* SettingsOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */; }; + 628952DC27E528CC00FDDCEF /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628952DB27E528CC00FDDCEF /* SettingsViewController.swift */; }; CA0536F626A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0536F526A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift */; }; CA238D1426A1153B000A36EC /* ContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA238D1326A1153B000A36EC /* ContentViewTests.swift */; }; CA4A6F2026CDAEE600BE3E74 /* TestEventReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4A6F1F26CDAEE600BE3E74 /* TestEventReceiver.swift */; }; @@ -112,6 +114,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsOnboardingViewController.swift; sourceTree = ""; }; + 628952DB27E528CC00FDDCEF /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; CA0536F526A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFeatureOnboardingView.swift; sourceTree = ""; }; CA238D1326A1153B000A36EC /* ContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewTests.swift; sourceTree = ""; }; CA4A6F1F26CDAEE600BE3E74 /* TestEventReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestEventReceiver.swift; sourceTree = ""; }; @@ -231,6 +235,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 628952D827E5280000FDDCEF /* Settings */ = { + isa = PBXGroup; + children = ( + 628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */, + 628952DB27E528CC00FDDCEF /* SettingsViewController.swift */, + ); + path = Settings; + sourceTree = ""; + }; CA0536F926A0917A00BF8FC5 /* Profile */ = { isa = PBXGroup; children = ( @@ -334,6 +347,7 @@ CAC34B6D26A07FE90039A373 /* Views */ = { isa = PBXGroup; children = ( + 628952D827E5280000FDDCEF /* Settings */, D72B763526FBCF5A00E0405F /* Design */, D78139CB270DE3AD004A4721 /* Map */, CA0536F926A0917A00BF8FC5 /* Profile */, @@ -663,6 +677,7 @@ CA7B829F26A1FAAC005AA87D /* InspectableAlert.swift in Sources */, CA0536F626A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift in Sources */, CAC34B4326A07F830039A373 /* SwiftUIExampleApp.swift in Sources */, + 628952DC27E528CC00FDDCEF /* SettingsViewController.swift in Sources */, D72B765526FC032B00E0405F /* ChangeEmailView.swift in Sources */, CA6FB0DE26C6AD5200FB3285 /* UIKitInteropProgrammaticViewController.swift in Sources */, CA7B821026A123F6005AA87D /* InspectableSheet.swift in Sources */, @@ -687,6 +702,7 @@ D72B765726FC036A00E0405F /* AccountInformationView.swift in Sources */, CAC34B7526A07FE90039A373 /* MapFeatureOnboardingView.swift in Sources */, D72B764926FBCFB200E0405F /* LoginView.swift in Sources */, + 628952DA27E5281700FDDCEF /* SettingsOnboardingViewController.swift in Sources */, D72B764326FBCF7000E0405F /* PasswordField.swift in Sources */, D7A6CE7E26E039C300599824 /* TestView.swift in Sources */, D72B764A26FBCFB200E0405F /* SignUp.swift in Sources */, diff --git a/ExampleApps/SwiftUIExample/SwiftUIExample/SwiftUIExampleApp.swift b/ExampleApps/SwiftUIExample/SwiftUIExample/SwiftUIExampleApp.swift index 94a1f0d99..084b8320d 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExample/SwiftUIExampleApp.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExample/SwiftUIExampleApp.swift @@ -21,11 +21,11 @@ struct SwiftUIExampleApp: App { if Environment.shouldTest { TestView() } else { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: SwiftCurrentOnboarding.self) { - thenProceed(with: ContentView.self) - .applyModifiers { $0.transition(.slide) } - }.applyModifiers { $0.transition(.slide) } + WorkflowView { + WorkflowItem(SwiftCurrentOnboarding.self) + .applyModifiers { $0.transition(.slide) } + WorkflowItem(ContentView.self) + .applyModifiers { $0.transition(.slide) } } .preferredColorScheme(.dark) } diff --git a/ExampleApps/SwiftUIExample/SwiftUIExample/TestViews/TestView.swift b/ExampleApps/SwiftUIExample/SwiftUIExample/TestViews/TestView.swift index 20c474dad..54731b7c4 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExample/TestViews/TestView.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExample/TestViews/TestView.swift @@ -22,14 +22,14 @@ struct TestView: View { @ViewBuilder var oneItemWorkflow: some View { if Environment.shouldEmbedInNavStack { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) + WorkflowView { + WorkflowItem(FR1.self) .persistence(persistence(for: FR1.self)) .presentationType(presentationType(for: FR1.self)) }.embedInNavigationView() } else { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) + WorkflowView { + WorkflowItem(FR1.self) .persistence(persistence(for: FR1.self)) .presentationType(presentationType(for: FR1.self)) } @@ -38,96 +38,84 @@ struct TestView: View { @ViewBuilder var twoItemWorkflow: some View { if Environment.shouldEmbedInNavStack { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - .persistence(persistence(for: FR2.self)) - .presentationType(presentationType(for: FR2.self)) - } - .persistence(persistence(for: FR1.self)) - .presentationType(presentationType(for: FR1.self)) + WorkflowView { + WorkflowItem(FR1.self) + .persistence(persistence(for: FR1.self)) + .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR2.self) + .persistence(persistence(for: FR2.self)) + .presentationType(presentationType(for: FR2.self)) }.embedInNavigationView() } else { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - .persistence(persistence(for: FR2.self)) - .presentationType(presentationType(for: FR2.self)) - } - .persistence(persistence(for: FR1.self)) - .presentationType(presentationType(for: FR1.self)) + WorkflowView { + WorkflowItem(FR1.self) + .persistence(persistence(for: FR1.self)) + .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR2.self) + .persistence(persistence(for: FR2.self)) + .presentationType(presentationType(for: FR2.self)) } } } @ViewBuilder var threeItemWorkflow: some View { if Environment.shouldEmbedInNavStack { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) - .persistence(persistence(for: FR2.self)) - .presentationType(presentationType(for: FR2.self)) - } + WorkflowView { + WorkflowItem(FR1.self) + .persistence(persistence(for: FR1.self)) + .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR2.self) + .persistence(persistence(for: FR2.self)) + .presentationType(presentationType(for: FR2.self)) + WorkflowItem(FR3.self) .persistence(persistence(for: FR2.self)) .presentationType(presentationType(for: FR2.self)) - } - .persistence(persistence(for: FR1.self)) - .presentationType(presentationType(for: FR1.self)) }.embedInNavigationView() } else { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) - .persistence(persistence(for: FR2.self)) - .presentationType(presentationType(for: FR2.self)) - } + WorkflowView { + WorkflowItem(FR1.self) + .persistence(persistence(for: FR1.self)) + .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR2.self) + .persistence(persistence(for: FR2.self)) + .presentationType(presentationType(for: FR2.self)) + WorkflowItem(FR3.self) .persistence(persistence(for: FR2.self)) .presentationType(presentationType(for: FR2.self)) - } - .persistence(persistence(for: FR1.self)) - .presentationType(presentationType(for: FR1.self)) } } } @ViewBuilder var fourItemWorkflow: some View { if Environment.shouldEmbedInNavStack { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - .persistence(persistence(for: FR4.self)) - .presentationType(presentationType(for: FR4.self)) - } - .persistence(persistence(for: FR3.self)) - .presentationType(presentationType(for: FR3.self)) - } + WorkflowView { + WorkflowItem(FR1.self) + .persistence(persistence(for: FR1.self)) + .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR2.self) .persistence(persistence(for: FR2.self)) .presentationType(presentationType(for: FR2.self)) - } - .persistence(persistence(for: FR1.self)) - .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR3.self) + .persistence(persistence(for: FR3.self)) + .presentationType(presentationType(for: FR3.self)) + WorkflowItem(FR4.self) + .persistence(persistence(for: FR4.self)) + .presentationType(presentationType(for: FR4.self)) }.embedInNavigationView() } else { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - .persistence(persistence(for: FR4.self)) - .presentationType(presentationType(for: FR4.self)) - } - .persistence(persistence(for: FR3.self)) - .presentationType(presentationType(for: FR3.self)) - } + WorkflowView { + WorkflowItem(FR1.self) + .persistence(persistence(for: FR1.self)) + .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR2.self) .persistence(persistence(for: FR2.self)) .presentationType(presentationType(for: FR2.self)) - } - .persistence(persistence(for: FR1.self)) - .presentationType(presentationType(for: FR1.self)) + WorkflowItem(FR3.self) + .persistence(persistence(for: FR3.self)) + .presentationType(presentationType(for: FR3.self)) + WorkflowItem(FR4.self) + .persistence(persistence(for: FR4.self)) + .presentationType(presentationType(for: FR4.self)) } } } diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/UIKitInteropTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/UIKitInteropTests.swift index c4f272550..154a2a92f 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/UIKitInteropTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/UIKitInteropTests.swift @@ -25,15 +25,16 @@ final class UIKitInteropTests: XCTestCase, View { let launchArgs = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: launchArgs) { - thenProceed(with: UIKitInteropProgrammaticViewController.self) + WorkflowView(launchingWith: launchArgs) { + WorkflowItem(UIKitInteropProgrammaticViewController.self) } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() try await MainActor.run { - let wrapper = try launcher.view(ViewControllerWrapper.self) + let wrapper = try launcher.find(ViewControllerWrapper.self) let context = unsafeBitCast(FakeContext(), to: UIViewControllerRepresentableContext>.self) var vc = try wrapper.actualView().makeUIViewController(context: context) vc.removeFromParent() @@ -68,17 +69,17 @@ final class UIKitInteropTests: XCTestCase, View { } let launchArgs = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: launchArgs) { - thenProceed(with: UIKitInteropProgrammaticViewController.self) { - thenProceed(with: FR1.self) - } + WorkflowView(launchingWith: launchArgs) { + WorkflowItem(UIKitInteropProgrammaticViewController.self) + WorkflowItem(FR1.self) } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() try await MainActor.run { - let wrapper = try launcher.view(ViewControllerWrapper.self) + let wrapper = try launcher.find(ViewControllerWrapper.self) let context = unsafeBitCast(FakeContext(), to: UIViewControllerRepresentableContext>.self) let vc = try wrapper.actualView().makeUIViewController(context: context) vc.removeFromParent() @@ -118,15 +119,16 @@ final class UIKitInteropTests: XCTestCase, View { } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) + WorkflowView { + WorkflowItem(FR1.self) } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() try await MainActor.run { - let wrapper = try workflowView.view(ViewControllerWrapper.self) + let wrapper = try workflowView.find(ViewControllerWrapper.self) let context = unsafeBitCast(FakeContext(), to: UIViewControllerRepresentableContext>.self) var vc = try wrapper.actualView().makeUIViewController(context: context) @@ -163,10 +165,9 @@ final class UIKitInteropTests: XCTestCase, View { required init?(coder: NSCoder) { nil } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } } .hostAndInspect(with: \.inspection) @@ -186,15 +187,16 @@ final class UIKitInteropTests: XCTestCase, View { func testPuttingAUIKitViewFromStoryboardInsideASwiftUIWorkflow() async throws { let launchArgs = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: launchArgs) { - thenProceed(with: TestInputViewController.self) + WorkflowView(launchingWith: launchArgs) { + WorkflowItem(TestInputViewController.self) } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() try await MainActor.run { - let wrapper = try launcher.view(ViewControllerWrapper.self) + let wrapper = try launcher.find(ViewControllerWrapper.self) let context = unsafeBitCast(FakeContext(), to: UIViewControllerRepresentableContext>.self) var vc = try wrapper.actualView().makeUIViewController(context: context) @@ -216,15 +218,16 @@ final class UIKitInteropTests: XCTestCase, View { func testPuttingAUIKitViewFromStoryboardThatDoesNotTakeInDataInsideASwiftUIWorkflow() async throws { let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: TestNoInputViewController.self) + WorkflowView { + WorkflowItem(TestNoInputViewController.self) } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() try await MainActor.run { - let wrapper = try launcher.view(ViewControllerWrapper.self) + let wrapper = try launcher.find(ViewControllerWrapper.self) let context = unsafeBitCast(FakeContext(), to: UIViewControllerRepresentableContext>.self) var vc = try wrapper.actualView().makeUIViewController(context: context) @@ -258,14 +261,14 @@ final class UIKitInteropTests: XCTestCase, View { } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertThrowsError(try launcher.view(ViewControllerWrapper.self)) XCTAssertEqual(try launcher.find(FR2.self).text().string(), "FR2") diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/ViewInspector/InspectableExtensions.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/ViewInspector/InspectableExtensions.swift index cdbcabc0e..d7242310a 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/ViewInspector/InspectableExtensions.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/ViewInspector/InspectableExtensions.swift @@ -22,6 +22,10 @@ extension MapFeatureOnboardingView: Inspectable { } extension MapFeatureView: Inspectable { } extension WorkflowItem: Inspectable { } extension WorkflowLauncher: Inspectable { } +extension WorkflowView: Inspectable { } +extension WorkflowItemWrapper: Inspectable { } +extension WorkflowGroup: Inspectable { } +extension ModalModifier: Inspectable { } extension ChangeEmailView: Inspectable { } extension ChangePasswordView: Inspectable { } extension QRScannerFeatureView: Inspectable { } diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/AccountInformationViewTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/AccountInformationViewTests.swift index cf4da271b..bb8924079 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/AccountInformationViewTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/AccountInformationViewTests.swift @@ -32,7 +32,7 @@ final class AccountInformationViewTests: XCTestCase, WorkflowTestingReceiver { Self.workflowLaunchedData.removeAll() } - private typealias MFAViewWorkflowView = WorkflowLauncher> + private typealias MFAViewWorkflowView = WorkflowLauncher, Never>> func testUpdatedAccountInformationView() async throws { let view = try await AccountInformationView().hostAndInspect(with: \.inspection) diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ChangePasswordViewTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ChangePasswordViewTests.swift index 536880901..6c3788a4e 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ChangePasswordViewTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ChangePasswordViewTests.swift @@ -29,13 +29,14 @@ final class ChangePasswordViewTests: XCTestCase, View { let currentPassword = UUID().uuidString let onFinish = expectation(description: "onFinish called") let view = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: currentPassword) { - thenProceed(with: ChangePasswordView.self) + WorkflowView(launchingWith: currentPassword) { + WorkflowItem(ChangePasswordView.self) } .onFinish { _ in onFinish.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertNoThrow(try view.find(ViewType.SecureField.self).setInput(currentPassword)) XCTAssertNoThrow(try view.find(ViewType.SecureField.self, skipFound: 1).setInput("asdfF1")) diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ContentViewTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ContentViewTests.swift index 048eb2bc7..509f40b44 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ContentViewTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ContentViewTests.swift @@ -14,10 +14,6 @@ import Swinject @testable import SwiftUIExample final class ContentViewTests: XCTestCase { - private typealias MapWorkflow = WorkflowLauncher, MapFeatureOnboardingView>> - private typealias QRScannerWorkflow = WorkflowLauncher, QRScannerFeatureOnboardingView>> - private typealias ProfileWorkflow = WorkflowLauncher, ProfileFeatureOnboardingView>> - override func setUpWithError() throws { Container.default.removeAll() } @@ -27,23 +23,52 @@ final class ContentViewTests: XCTestCase { Container.default.register(UserDefaults.self) { _ in defaults } let contentView = try await ContentView().hostAndInspect(with: \.inspection) - let wf1 = try contentView.tabView().view(MapWorkflow.self, 0).actualView() - XCTAssertEqual(try contentView.tabView().view(MapWorkflow.self, 0).tabItem().label().title().text().string(), "Map") - let wf2 = try contentView.tabView().view(QRScannerWorkflow.self, 1).actualView() - XCTAssertEqual(try contentView.tabView().view(QRScannerWorkflow.self, 1).tabItem().label().title().text().string(), "QR Scanner") - let wf3 = try contentView.tabView().view(ProfileWorkflow.self, 2).actualView() - XCTAssertEqual(try contentView.tabView().view(ProfileWorkflow.self, 2).tabItem().label().title().text().string(), "Profile") - - let wfr1 = try await wf1.hostAndInspect(with: \.inspection) + let wf1 = try await MainActor.run { + try contentView.tabView().workflow(0) { + WorkflowItem(MapFeatureOnboardingView.self) + WorkflowItem(MapFeatureView.self) + } + } + XCTAssertEqual(try wf1.tabItem().label().title().text().string(), "Map") + let wf2 = try await MainActor.run { + try contentView.tabView().workflow(1) { + WorkflowItem(QRScannerFeatureOnboardingView.self) + WorkflowItem(QRScannerFeatureView.self) + } + } + XCTAssertEqual(try wf2.tabItem().label().title().text().string(), "QR Scanner") + let wf3 = try await MainActor.run { + try contentView.tabView().workflow(2) { + WorkflowItem(ProfileFeatureOnboardingView.self) + WorkflowItem(ProfileFeatureView.self) + } + } + XCTAssertEqual(try wf3.tabItem().label().title().text().string(), "Profile") + + let wfr1 = try await wf1.actualView().hostAndInspect(with: \.inspection) try await wfr1.find(MapFeatureOnboardingView.self).proceedInWorkflow() XCTAssertNoThrow(try wfr1.find(MapFeatureView.self)) - let wfr2 = try await wf2.hostAndInspect(with: \.inspection) + let wfr2 = try await wf2.actualView().hostAndInspect(with: \.inspection) try await wfr2.find(QRScannerFeatureOnboardingView.self).proceedInWorkflow() XCTAssertNoThrow(try wfr2.find(QRScannerFeatureView.self)) - let wfr3 = try await wf3.hostAndInspect(with: \.inspection) + let wfr3 = try await wf3.actualView().hostAndInspect(with: \.inspection) try await wfr3.find(ProfileFeatureOnboardingView.self).proceedInWorkflow() XCTAssertNoThrow(try wfr3.find(ProfileFeatureView.self)) } } + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension InspectableView where View: SingleViewContent { + func workflow(@WorkflowBuilder builder: () -> T) throws -> InspectableView>>> { + try view(WorkflowView>.self) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension InspectableView where View: MultipleViewContent { + func workflow(_ index: Int, @WorkflowBuilder builder: () -> T) throws -> InspectableView>>> { + try view(WorkflowView>.self, index) + } +} diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/GenericOnboardingViewTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/GenericOnboardingViewTests.swift index 803a512e8..90a23fdf2 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/GenericOnboardingViewTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/GenericOnboardingViewTests.swift @@ -32,14 +32,15 @@ final class GenericOnboardingViewTests: XCTestCase, View { Container.default.register(UserDefaults.self) { _ in defaults } let workflowFinished = expectation(description: "View Proceeded") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: defaultModel) { - thenProceed(with: GenericOnboardingView.self) + WorkflowView(launchingWith: defaultModel) { + WorkflowItem(GenericOnboardingView.self) }.onFinish { _ in workflowFinished.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertNoThrow(try launcher.find(ViewType.Text.self)) XCTAssertEqual(try launcher.find(ViewType.Text.self).string(), self.defaultModel.featureTitle) diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/LoginTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/LoginTests.swift index 0af34dc1b..801c31b50 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/LoginTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/LoginTests.swift @@ -40,14 +40,15 @@ final class LoginTests: XCTestCase, View, WorkflowTestingReceiver { func testLoginProceedsWorkflow() async throws { let workflowFinished = expectation(description: "View Proceeded") let view = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: LoginView.self) + WorkflowView { + WorkflowItem(LoginView.self) }.onFinish { _ in workflowFinished.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertNoThrow(try view.findLoginButton().tap()) diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/MapFeatureOnboardingViewTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/MapFeatureOnboardingViewTests.swift index b54cca0f7..b08e40bd9 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/MapFeatureOnboardingViewTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/MapFeatureOnboardingViewTests.swift @@ -27,14 +27,15 @@ final class MapFeatureOnboardingViewTests: XCTestCase, View { let workflowFinished = expectation(description: "View Proceeded") let view = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: MapFeatureOnboardingView.self) + WorkflowView { + WorkflowItem(MapFeatureOnboardingView.self) }.onFinish { _ in workflowFinished.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertNoThrow(try view.find(ViewType.Text.self)) XCTAssertEqual(try view.find(ViewType.Text.self).string(), "Learn about our awesome map feature!") diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ProfileFeatureOnboardingViewTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ProfileFeatureOnboardingViewTests.swift index 32c7f90da..9f741383a 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ProfileFeatureOnboardingViewTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/ProfileFeatureOnboardingViewTests.swift @@ -26,8 +26,8 @@ final class ProfileFeatureOnboardingViewTests: XCTestCase, View { Container.default.register(UserDefaults.self) { _ in defaults } let workflowFinished = expectation(description: "View Proceeded") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: ProfileFeatureOnboardingView.self) + WorkflowView { + WorkflowItem(ProfileFeatureOnboardingView.self) }.onFinish { _ in workflowFinished.fulfill() } diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/QRScannerFeatureOnboardingViewTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/QRScannerFeatureOnboardingViewTests.swift index fc2e8c16e..641ee58cd 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/QRScannerFeatureOnboardingViewTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/QRScannerFeatureOnboardingViewTests.swift @@ -26,14 +26,15 @@ final class QRScannerFeatureOnboardingViewTests: XCTestCase, View { Container.default.register(UserDefaults.self) { _ in defaults } let workflowFinished = expectation(description: "View Proceeded") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: QRScannerFeatureOnboardingView.self) + WorkflowView { + WorkflowItem(QRScannerFeatureOnboardingView.self) }.onFinish { _ in workflowFinished.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertNoThrow(try launcher.find(ViewType.Text.self)) XCTAssertEqual(try launcher.find(ViewType.Text.self).string(), "Learn about our awesome QR scanning feature!") diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SignUpTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SignUpTests.swift index aa2782896..701d52f52 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SignUpTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SignUpTests.swift @@ -25,14 +25,15 @@ final class SignUpTests: XCTestCase, View { func testContinueProceedsWorkflow() async throws { let workflowFinished = expectation(description: "View Proceeded") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: SignUp.self) + WorkflowView { + WorkflowItem(SignUp.self) }.onFinish { _ in workflowFinished.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertNoThrow(try launcher.findProceedButton().tap()) wait(for: [workflowFinished], timeout: TestConstant.timeout) diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SwiftCurrentOnboardingTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SwiftCurrentOnboardingTests.swift index 6f0c35c7c..485398a65 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SwiftCurrentOnboardingTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/SwiftCurrentOnboardingTests.swift @@ -26,14 +26,15 @@ final class SwiftCurrentOnboardingTests: XCTestCase, View { Container.default.register(UserDefaults.self) { _ in defaults } let workflowFinished = expectation(description: "View Proceeded") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: SwiftCurrentOnboarding.self) + WorkflowView { + WorkflowItem(SwiftCurrentOnboarding.self) }.onFinish { _ in workflowFinished.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() XCTAssertNoThrow(try launcher.find(ViewType.Button.self).tap()) diff --git a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/TermsAndConditionsTests.swift b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/TermsAndConditionsTests.swift index f311999cd..74ac3cc3f 100644 --- a/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/TermsAndConditionsTests.swift +++ b/ExampleApps/SwiftUIExample/SwiftUIExampleTests/Views/TermsAndConditionsTests.swift @@ -23,16 +23,17 @@ final class TermsAndConditionsTests: XCTestCase, View { func testPrimaryAcceptButtonCompletesWorkflow() async throws { let workflowFinished = expectation(description: "View Proceeded") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: TermsAndConditions.self) + WorkflowView { + WorkflowItem(TermsAndConditions.self) }.onAbandon { XCTFail("Abandon should not have been called") }.onFinish { _ in workflowFinished.fulfill() } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() let primaryButton = try launcher.find(PrimaryButton.self) // ToS should have a primary call to accept XCTAssertEqual(try primaryButton.find(ViewType.Text.self).string(), "Accept") @@ -44,16 +45,17 @@ final class TermsAndConditionsTests: XCTestCase, View { func testSecondaryRejectButtonAbandonsWorkflow() async throws { let workflowAbandoned = expectation(description: "View Proceeded") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: TermsAndConditions.self) + WorkflowView { + WorkflowItem(TermsAndConditions.self) }.onAbandon { workflowAbandoned.fulfill() }.onFinish { _ in XCTFail("Complete should not have been called") } } - .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .content + .hostAndInspect(with: \.inspection) + .extractWorkflowItemWrapper() let secondaryButton = try launcher.find(SecondaryButton.self) // ToS sould have a secondary call to decline XCTAssertEqual(try secondaryButton.find(ViewType.Text.self).string(), "Decline") diff --git a/ExampleApps/SwiftUIExample/Views/ContentView.swift b/ExampleApps/SwiftUIExample/Views/ContentView.swift index 90e7da283..2d2b48ec3 100644 --- a/ExampleApps/SwiftUIExample/Views/ContentView.swift +++ b/ExampleApps/SwiftUIExample/Views/ContentView.swift @@ -16,38 +16,44 @@ struct ContentView: View, FlowRepresentable { case map case qr case profile + case settings } @State var selectedTab: Tab = .map weak var _workflowPointer: AnyFlowRepresentable? var body: some View { - TabView(selection: $selectedTab) { + TabView(selection: $selectedTab) { // swiftlint:disable:this closure_body_length // NOTE: Using constant here guarantees the workflow cannot abandon, it stays launched forever. - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: MapFeatureOnboardingView.self) { - thenProceed(with: MapFeatureView.self) - } + WorkflowView { + WorkflowItem(MapFeatureOnboardingView.self) + WorkflowItem(MapFeatureView.self) }.tabItem { Label("Map", systemImage: "map") } .tag(Tab.map) - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: QRScannerFeatureOnboardingView.self) { - thenProceed(with: QRScannerFeatureView.self) - } + WorkflowView { + WorkflowItem(QRScannerFeatureOnboardingView.self) + WorkflowItem(QRScannerFeatureView.self) }.tabItem { Label("QR Scanner", systemImage: "camera") } .tag(Tab.qr) - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: ProfileFeatureOnboardingView.self) { - thenProceed(with: ProfileFeatureView.self) - } + WorkflowView { + WorkflowItem(ProfileFeatureOnboardingView.self) + WorkflowItem(ProfileFeatureView.self) }.tabItem { Label("Profile", systemImage: "person.crop.circle") } .tag(Tab.profile) + + WorkflowView { + WorkflowItem(SettingsOnboardingViewController.self) + WorkflowItem(SettingsViewController.self) + }.tabItem { + Label("Settings", systemImage: "gear.circle.fill") + } + .tag(Tab.settings) } .onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector } diff --git a/ExampleApps/SwiftUIExample/Views/LoginView.swift b/ExampleApps/SwiftUIExample/Views/LoginView.swift index 41d3c187c..3a56b1dcc 100644 --- a/ExampleApps/SwiftUIExample/Views/LoginView.swift +++ b/ExampleApps/SwiftUIExample/Views/LoginView.swift @@ -72,11 +72,11 @@ struct LoginView: View, FlowRepresentable { } .background(Color.primaryBackground.edgesIgnoringSafeArea(.all)) .sheet(isPresented: $showSignUp) { - WorkflowLauncher(isLaunched: $showSignUp) { - thenProceed(with: SignUp.self) { - thenProceed(with: TermsAndConditions.self) - .presentationType(.navigationLink) - }.presentationType(.navigationLink) + WorkflowView(isLaunched: $showSignUp) { + WorkflowItem(SignUp.self) + .presentationType(.navigationLink) + WorkflowItem(TermsAndConditions.self) + .presentationType(.navigationLink) } .embedInNavigationView() .onFinish { _ in diff --git a/ExampleApps/SwiftUIExample/Views/Profile/AccountInformationView.swift b/ExampleApps/SwiftUIExample/Views/Profile/AccountInformationView.swift index 70d0e0797..2df4715f3 100644 --- a/ExampleApps/SwiftUIExample/Views/Profile/AccountInformationView.swift +++ b/ExampleApps/SwiftUIExample/Views/Profile/AccountInformationView.swift @@ -42,10 +42,9 @@ struct AccountInformationView: View, FlowRepresentable { } .textEntryStyle() } else { - WorkflowLauncher(isLaunched: $emailWorkflowLaunched.animation(), startingArgs: email) { - thenProceed(with: MFAView.self) { - thenProceed(with: ChangeEmailView.self) - } + WorkflowView(isLaunched: $emailWorkflowLaunched.animation(), launchingWith: email) { + WorkflowItem(MFAView.self) + WorkflowItem(ChangeEmailView.self) }.onFinish { guard case .args(let newEmail as String) = $0 else { return } email = newEmail @@ -75,25 +74,24 @@ struct AccountInformationView: View, FlowRepresentable { } .textEntryStyle() } else { - WorkflowLauncher(isLaunched: $passwordWorkflowLaunched.animation(), startingArgs: password) { - thenProceed(with: MFAView.self) { - thenProceed(with: ChangePasswordView.self) - .presentationType(.modal) - .applyModifiers { cpv in - NavigationView { - VStack { - cpv - .padding() - .background(Color.card) - .cornerRadius(35) - .padding(.horizontal, 20) - .navigationTitle("Update password") + WorkflowView(isLaunched: $passwordWorkflowLaunched.animation(), launchingWith: password) { + WorkflowItem(MFAView.self) + WorkflowItem(ChangePasswordView.self) + .presentationType(.modal) + .applyModifiers { cpv in + NavigationView { + VStack { + cpv + .padding() + .background(Color.card) + .cornerRadius(35) + .padding(.horizontal, 20) + .navigationTitle("Update password") - Spacer() - } + Spacer() } } - } + } }.onFinish { guard case .args(let newPassword as String) = $0 else { return } password = newPassword diff --git a/ExampleApps/SwiftUIExample/Views/Settings/SettingsOnboardingViewController.swift b/ExampleApps/SwiftUIExample/Views/Settings/SettingsOnboardingViewController.swift new file mode 100644 index 000000000..f17968804 --- /dev/null +++ b/ExampleApps/SwiftUIExample/Views/Settings/SettingsOnboardingViewController.swift @@ -0,0 +1,38 @@ +// +// SettingsOnboardingViewController.swift +// SwiftCurrent +// +// Created by Nick Kaczmarek on 3/18/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import Foundation +import UIKit +import SwiftCurrent +import SwiftCurrent_UIKit + +final class SettingsOnboardingViewController: UIWorkflowItem, FlowRepresentable { // SwiftCurrent + typealias WorkflowOutput = String + let nextButton = UIButton() + let onboardingLabel = UILabel() + + @objc private func nextPressed() { + proceedInWorkflow("Check out all these settings!") + } + + override func viewDidLoad() { + nextButton.setTitle("Continue", for: .normal) + nextButton.setTitleColor(.systemBlue, for: .normal) + nextButton.addTarget(self, action: #selector(nextPressed), for: .touchUpInside) + view.addSubview(nextButton) + nextButton.translatesAutoresizingMaskIntoConstraints = false + nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + nextButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + + onboardingLabel.text = "This is a settings onboarding view in UIKit" + view.addSubview(onboardingLabel) + onboardingLabel.translatesAutoresizingMaskIntoConstraints = false + onboardingLabel.centerXAnchor.constraint(equalTo: nextButton.centerXAnchor).isActive = true + onboardingLabel.centerYAnchor.constraint(equalTo: nextButton.centerYAnchor, constant: -44).isActive = true + } +} diff --git a/ExampleApps/SwiftUIExample/Views/Settings/SettingsViewController.swift b/ExampleApps/SwiftUIExample/Views/Settings/SettingsViewController.swift new file mode 100644 index 000000000..0d4d6c016 --- /dev/null +++ b/ExampleApps/SwiftUIExample/Views/Settings/SettingsViewController.swift @@ -0,0 +1,32 @@ +// +// SettingsViewController.swift +// SwiftCurrent +// +// Created by Nick Kaczmarek on 3/18/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import Foundation +import UIKit +import SwiftCurrent +import SwiftCurrent_UIKit + +final class SettingsViewController: UIWorkflowItem, FlowRepresentable { + required init(with args: String) { // SwiftCurrent + inputArgs = args + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + let inputArgs: String + let onboardingLabel = UILabel() + + override func viewDidLoad() { + onboardingLabel.text = inputArgs + view.addSubview(onboardingLabel) + onboardingLabel.translatesAutoresizingMaskIntoConstraints = false + onboardingLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + onboardingLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + } +} diff --git a/README.md b/README.md index aaa3e8f35..b6cf80969 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,9 @@ import SwiftCurrent_SwiftUI // ... var body: some View { // ... other view code (if any) - WorkflowLauncher(isLaunched: .constant(true), startingArgs: "Skip optional screen") { - thenProceed(with: OptionalView.self) { - thenProceed(with: ExampleView.self) - } + WorkflowView(launchingWith: "Skip optional screen") { + WorkflowItem(OptionalView.self) + WorkflowItem(ExampleView.self) } } ``` diff --git a/Sources/SwiftCurrent_SwiftUI/Deprecations/WorkflowLauncherDeprecations.swift b/Sources/SwiftCurrent_SwiftUI/Deprecations/WorkflowLauncherDeprecations.swift new file mode 100644 index 000000000..05cddae8a --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Deprecations/WorkflowLauncherDeprecations.swift @@ -0,0 +1,65 @@ +// swiftlint:disable:this file_name +// WorkflowLauncherDeprecations.swift +// SwiftCurrent +// +// Created by Nick Kaczmarek on 3/18/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import Foundation +import SwiftCurrent +import SwiftUI + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension View { + /// :nodoc: thenProceed deprecation + @available(*, unavailable, renamed: "WorkflowItem(_:)") + public func thenProceed(with: F.Type) -> Never { + fatalError("Obsoleted") + } + + /// :nodoc: thenProceed deprecation + @available(*, unavailable, message: "thenProceed has been removed in favor of WorkflowItem(_:). See docs for usages. https://wwt.github.io/SwiftCurrent/Creating%20Workflows%20in%20SwiftUI.html#step-2") + public func thenProceed(with: F.Type, _: () -> V) -> Never { + fatalError("Obsoleted") + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension App { + /// :nodoc: thenProceed deprecation + @available(*, unavailable, renamed: "WorkflowItem(_:)") + public func thenProceed(with: F.Type) -> Never { + fatalError("Obsoleted") + } + + /// :nodoc: thenProceed deprecation + @available(*, unavailable, message: "thenProceed has been removed in favor of WorkflowItem(_:). See docs for usages. https://wwt.github.io/SwiftCurrent/Creating%20Workflows%20in%20SwiftUI.html#step-2") + public func thenProceed(with: F.Type, _: () -> V) -> Never { + fatalError("Obsoleted") + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension Scene { + /// :nodoc: thenProceed deprecation + @available(*, unavailable, renamed: "WorkflowItem(_:)") + public func thenProceed(with: F.Type) -> Never { + fatalError("Obsoleted") + } + + /// :nodoc: thenProceed deprecation + @available(*, unavailable, message: "thenProceed has been removed in favor of WorkflowItem(_:). See docs for usages. https://wwt.github.io/SwiftCurrent/Creating%20Workflows%20in%20SwiftUI.html#step-2") + public func thenProceed(with: F.Type, _: () -> V) -> Never { + fatalError("Obsoleted") + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension WorkflowLauncher { + /// :nodoc: WorkflowLauncher deprecation + @available(*, unavailable, renamed: "WorkflowView(isLaunched:launchingWith:_:)") + public init(isLaunched: Binding, startingArgs: T, _: () -> Content) { + fatalError("Obsoleted") + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/Extensions/ThenProceedExtensions.swift b/Sources/SwiftCurrent_SwiftUI/Extensions/ThenProceedExtensions.swift deleted file mode 100644 index 0411c9742..000000000 --- a/Sources/SwiftCurrent_SwiftUI/Extensions/ThenProceedExtensions.swift +++ /dev/null @@ -1,162 +0,0 @@ -// swiftlint:disable:this file_name -// ThenProceedExtensions.swift -// SwiftCurrent_SwiftUI -// -// Created by Brian Lombardo on 8/24/21. -// Copyright © 2021 WWT and Tyler Thompson. All rights reserved. -// -// swiftlint:disable line_length - -import Foundation -import SwiftCurrent -import SwiftUI - -private func verifyWorkflowIsWellFormed(_ lhs: LHS.Type, _ rhs: RHS.Type) { - guard !(RHS.WorkflowInput.self is Never.Type), // an input type of `Never` indicates any arguments will simply be ignored - !(RHS.WorkflowInput.self is AnyWorkflow.PassedArgs.Type), // an input type of `AnyWorkflow.PassedArgs` means either some value or no value can be passed - !(LHS.WorkflowOutput.self is AnyWorkflow.PassedArgs.Type) else { return } // an output type of `AnyWorkflow.PassedArgs` can only be checked at runtime when the actual value is passed forward - - // trap if workflow is malformed (output does not match input) - assert(LHS.WorkflowOutput.self is RHS.WorkflowInput.Type, "Workflow is malformed, expected output of: \(LHS.self) (\(LHS.WorkflowOutput.self)) to match input of: \(RHS.self) (\(RHS.WorkflowInput.self)") -} - -@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -extension View { - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - public func thenProceed(with: FR.Type) -> WorkflowItem { - WorkflowItem(FR.self) - } - - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Parameter nextItem: a closure returning the next item in the `Workflow`. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - public func thenProceed(with: FR.Type, nextItem: () -> WorkflowItem) -> WorkflowItem, FR> { - verifyWorkflowIsWellFormed(FR.self, F.self) - return WorkflowItem(FR.self) { nextItem() } - } - - #if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)) && canImport(UIKit) - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - @available(iOS 14.0, macOS 11, tvOS 14.0, *) - public func thenProceed(with: VC.Type) -> WorkflowItem, Never, ViewControllerWrapper> { - WorkflowItem(VC.self) - } - - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Parameter nextItem: a closure returning the next item in the `Workflow`. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - @available(iOS 14.0, macOS 11, tvOS 14.0, *) - public func thenProceed(with: VC.Type, nextItem: () -> WorkflowItem) -> WorkflowItem, WorkflowItem, ViewControllerWrapper> { - verifyWorkflowIsWellFormed(VC.self, F.self) - return WorkflowItem(VC.self) { nextItem() } - } - #endif -} - -@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -extension App { - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - public func thenProceed(with: FR.Type) -> WorkflowItem { - WorkflowItem(FR.self) - } - - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Parameter nextItem: a closure returning the next item in the `Workflow`. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - public func thenProceed(with: FR.Type, nextItem: () -> WorkflowItem) -> WorkflowItem, FR> { - verifyWorkflowIsWellFormed(FR.self, F.self) - return WorkflowItem(FR.self) { nextItem() } - } - - #if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)) && canImport(UIKit) - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - @available(iOS 14.0, macOS 11, tvOS 14.0, *) - public func thenProceed(with: VC.Type) -> WorkflowItem, Never, ViewControllerWrapper> { - WorkflowItem(VC.self) - } - - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Parameter nextItem: a closure returning the next item in the `Workflow`. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - @available(iOS 14.0, macOS 11, tvOS 14.0, *) - public func thenProceed(with: VC.Type, nextItem: () -> WorkflowItem) -> WorkflowItem, WorkflowItem, ViewControllerWrapper> { - verifyWorkflowIsWellFormed(VC.self, F.self) - return WorkflowItem(VC.self) { nextItem() } - } - #endif -} - -@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -extension Scene { - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - public func thenProceed(with: FR.Type) -> WorkflowItem { - WorkflowItem(FR.self) - } - - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Parameter nextItem: a closure returning the next item in the `Workflow`. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - public func thenProceed(with: FR.Type, nextItem: () -> WorkflowItem) -> WorkflowItem, FR> { - verifyWorkflowIsWellFormed(FR.self, F.self) - return WorkflowItem(FR.self) { nextItem() } - } - - #if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)) && canImport(UIKit) - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - @available(iOS 14.0, macOS 11, tvOS 14.0, *) - public func thenProceed(with: VC.Type) -> WorkflowItem, Never, ViewControllerWrapper> { - WorkflowItem(VC.self) - } - - /** - Adds an item to the workflow; enforces the `FlowRepresentable.WorkflowOutput` of the previous item matches the args that will be passed forward. - - Parameter with: a `FlowRepresentable` type that should be presented. - - Parameter nextItem: a closure returning the next item in the `Workflow`. - - Returns: a new `WorkflowItem` with the additional `FlowRepresentable` item. - */ - @available(iOS 14.0, macOS 11, tvOS 14.0, *) - public func thenProceed(with: VC.Type, nextItem: () -> WorkflowItem) -> WorkflowItem, WorkflowItem, ViewControllerWrapper> { - verifyWorkflowIsWellFormed(VC.self, F.self) - return WorkflowItem(VC.self) { nextItem() } - } - #endif -} diff --git a/Sources/SwiftCurrent_SwiftUI/Protocols/WorkflowItemPresentable.swift b/Sources/SwiftCurrent_SwiftUI/Protocols/WorkflowItemPresentable.swift deleted file mode 100644 index 88548233a..000000000 --- a/Sources/SwiftCurrent_SwiftUI/Protocols/WorkflowItemPresentable.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// WorkflowItemPresentable.swift -// SwiftCurrent_SwiftUI -// -// Created by Morgan Zellers on 8/31/21. -// Copyright © 2021 WWT and Tyler Thompson. All rights reserved. -// - -import SwiftCurrent - -@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -protocol WorkflowItemPresentable { - var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { get } -} diff --git a/Sources/SwiftCurrent_SwiftUI/Protocols/WorkflowModifier.swift b/Sources/SwiftCurrent_SwiftUI/Protocols/WorkflowModifier.swift deleted file mode 100644 index 8184cc31c..000000000 --- a/Sources/SwiftCurrent_SwiftUI/Protocols/WorkflowModifier.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// WorkflowModifier.swift -// SwiftCurrent_SwiftUI -// -// Created by Tyler Thompson on 8/21/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, *) -protocol WorkflowModifier { - func modify(workflow: AnyWorkflow) -} diff --git a/Sources/SwiftCurrent_SwiftUI/Protocols/_WorkflowItemProtocol.swift b/Sources/SwiftCurrent_SwiftUI/Protocols/_WorkflowItemProtocol.swift new file mode 100644 index 000000000..02569bf77 --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Protocols/_WorkflowItemProtocol.swift @@ -0,0 +1,45 @@ +// +// _WorkflowItemProtocol.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 2/23/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +/// :nodoc: Protocol is forced to be public, but it is an internal protocol. +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public protocol _WorkflowItemProtocol: View where FlowRepresentableType: FlowRepresentable & View { + associatedtype FlowRepresentableType + + var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { get } + + func canDisplay(_ element: AnyWorkflow.Element?) -> Bool + mutating func setElementRef(_ element: AnyWorkflow.Element?) + func modify(workflow: AnyWorkflow) +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension _WorkflowItemProtocol { + /// :nodoc: Protocol requirement. + public func setElementRef(_ element: AnyWorkflow.Element?) { } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension Never: _WorkflowItemProtocol { + /// :nodoc: Protocol requirement. + public typealias FlowRepresentableType = Never + + /// :nodoc: Protocol requirement. + public typealias Content = Never + + /// :nodoc: Protocol requirement. + public var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { .default } + + /// :nodoc: Protocol requirement. + public func canDisplay(_ element: AnyWorkflow.Element?) -> Bool { false } + /// :nodoc: Protocol requirement. + public func modify(workflow: AnyWorkflow) { } +} diff --git a/Sources/SwiftCurrent_SwiftUI/ResultBuilders/WorkflowBuilder.swift b/Sources/SwiftCurrent_SwiftUI/ResultBuilders/WorkflowBuilder.swift new file mode 100644 index 000000000..c9c53a16e --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/ResultBuilders/WorkflowBuilder.swift @@ -0,0 +1,236 @@ +// +// WorkflowBuilder.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 2/21/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// swiftlint:disable line_length +// swiftlint:disable operator_usage_whitespace +// swiftlint BUG: https://github.com/realm/SwiftLint/issues/3668 + +import Foundation + +/** + Used to build a `Workflow` in SwiftUI; Embed `WorkflowItem`s in a `WorkflowBuilder` to define your workflow. + + ### Discussion + Typically, you'll use this when you use `WorkflowView`. Otherwise you might use it as a way to build your own workflows in a wrapper type. + + #### Example + ```swift + WorkflowView(isLaunched: $isLaunched.animation(), launchingWith: "String in") { + WorkflowItem(FirstView.self) + WorkflowItem(SecondView.self) + } + .onAbandon { print("isLaunched is now false") } + .onFinish { args in print("Finished 1: \(args)") } + .onFinish { print("Finished 2: \($0)") } + .background(Color.green) + ``` + + #### NOTE + There is a Swift-imposed limit on how many items we can have in a `WorkflowBuilder`. Similar to SwiftUI's ViewBuilder, `WorkflowBuilder` has a limit of 10 items. Just like you can use `Group` in SwiftUI you can use `WorkflowGroup` to get around that 10 item limit with SwiftCurrent. + */ +@resultBuilder +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public enum WorkflowBuilder { + // swiftlint:disable:next missing_docs + public static func buildOptional(_ component: W?) -> OptionalWorkflowItem { + OptionalWorkflowItem(content: component) + } + + // swiftlint:disable:next missing_docs + public static func buildEither(first component: TrueCondition) -> EitherWorkflowItem { + .init(content: .first(component)) + } + + // swiftlint:disable:next missing_docs + public static func buildEither(second component: FalseCondition) -> EitherWorkflowItem { + .init(content: .second(component)) + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0) -> WorkflowItemWrapper { + WorkflowItemWrapper(content: w0) + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1) -> WorkflowItemWrapper> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2) -> WorkflowItemWrapper>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) + } + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2, _ w3: W3) -> WorkflowItemWrapper>>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) { + WorkflowItemWrapper(content: w3) + } + } + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2, _ w3: W3, _ w4: W4) -> WorkflowItemWrapper>>>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) { + WorkflowItemWrapper(content: w3) { + WorkflowItemWrapper(content: w4) + } + } + } + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2, _ w3: W3, _ w4: W4, _ w5: W5) -> WorkflowItemWrapper>>>>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) { + WorkflowItemWrapper(content: w3) { + WorkflowItemWrapper(content: w4) { + WorkflowItemWrapper(content: w5) + } + } + } + } + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2, _ w3: W3, _ w4: W4, _ w5: W5, _ w6: W6) -> WorkflowItemWrapper>>>>>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) { + WorkflowItemWrapper(content: w3) { + WorkflowItemWrapper(content: w4) { + WorkflowItemWrapper(content: w5) { + WorkflowItemWrapper(content: w6) + } + } + } + } + } + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2, _ w3: W3, _ w4: W4, _ w5: W5, _ w6: W6, _ w7: W7) -> WorkflowItemWrapper>>>>>>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) { + WorkflowItemWrapper(content: w3) { + WorkflowItemWrapper(content: w4) { + WorkflowItemWrapper(content: w5) { + WorkflowItemWrapper(content: w6) { + WorkflowItemWrapper(content: w7) + } + } + } + } + } + } + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2, _ w3: W3, _ w4: W4, _ w5: W5, _ w6: W6, _ w7: W7, _ w8: W8) -> WorkflowItemWrapper>>>>>>>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) { + WorkflowItemWrapper(content: w3) { + WorkflowItemWrapper(content: w4) { + WorkflowItemWrapper(content: w5) { + WorkflowItemWrapper(content: w6) { + WorkflowItemWrapper(content: w7) { + WorkflowItemWrapper(content: w8) + } + } + } + } + } + } + } + } + } + + // swiftlint:disable:next missing_docs + public static func buildBlock(_ w0: W0, _ w1: W1, _ w2: W2, _ w3: W3, _ w4: W4, _ w5: W5, _ w6: W6, _ w7: W7, _ w8: W8, _ w9: W9) -> WorkflowItemWrapper>>>>>>>>> { + WorkflowItemWrapper(content: w0) { + WorkflowItemWrapper(content: w1) { + WorkflowItemWrapper(content: w2) { + WorkflowItemWrapper(content: w3) { + WorkflowItemWrapper(content: w4) { + WorkflowItemWrapper(content: w5) { + WorkflowItemWrapper(content: w6) { + WorkflowItemWrapper(content: w7) { + WorkflowItemWrapper(content: w8) { + WorkflowItemWrapper(content: w9) + } + } + } + } + } + } + } + } + } + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/ViewModifiers/ModalModifier.swift b/Sources/SwiftCurrent_SwiftUI/ViewModifiers/ModalModifier.swift new file mode 100644 index 000000000..aa987efeb --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/ViewModifiers/ModalModifier.swift @@ -0,0 +1,33 @@ +// +// ModalModifier.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 3/14/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +struct ModalModifier: ViewModifier { + @Binding var isPresented: Bool + @State var modalStyle: LaunchStyle.SwiftUI.ModalPresentationStyle + @State var destination: V + + func body(content: Self.Content) -> some View { + switch modalStyle { + case .sheet: content.testableSheet(isPresented: $isPresented) { destination } + #if (os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)) + case .fullScreenCover: content.fullScreenCover(isPresented: $isPresented) { destination } + #endif + } + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension View { + func modal(isPresented: Binding, style: LaunchStyle.SwiftUI.ModalPresentationStyle, destination: V) -> some View { + modifier(ModalModifier(isPresented: isPresented, modalStyle: style, destination: destination)) + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/Views/NavigationWrapper.swift b/Sources/SwiftCurrent_SwiftUI/ViewModifiers/NavigationWrapper.swift similarity index 100% rename from Sources/SwiftCurrent_SwiftUI/Views/NavigationWrapper.swift rename to Sources/SwiftCurrent_SwiftUI/ViewModifiers/NavigationWrapper.swift diff --git a/Sources/SwiftCurrent_SwiftUI/Views/EitherWorkflowItem.swift b/Sources/SwiftCurrent_SwiftUI/Views/EitherWorkflowItem.swift new file mode 100644 index 000000000..52e547ea6 --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Views/EitherWorkflowItem.swift @@ -0,0 +1,75 @@ +// +// EitherWorkflowItem.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 3/17/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +/// :nodoc: ResultBuilder requirement. +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public struct EitherWorkflowItem: View, _WorkflowItemProtocol where W0.FlowRepresentableType.WorkflowInput == W1.FlowRepresentableType.WorkflowInput { + enum Either: View where First: _WorkflowItemProtocol, Second: _WorkflowItemProtocol { + var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { + switch self { + case .first(let first): return first.workflowLaunchStyle + case .second(let second): return second.workflowLaunchStyle + } + } + + func canDisplay(_ element: AnyWorkflow.Element?) -> Bool { + switch self { + case .first(let first): return first.canDisplay(element) + case .second(let second): return second.canDisplay(element) + } + } + + func modify(workflow: AnyWorkflow) { + switch self { + case .first(let first): first.modify(workflow: workflow) + case .second(let second): second.modify(workflow: workflow) + } + } + + case first(First) + case second(Second) + + var body: some View { + switch self { + case .first(let first): first + case .second(let second): second + } + } + } + + /// :nodoc: Protocol requirement. + public typealias FlowRepresentableType = W0.FlowRepresentableType + + @State var content: Either + + /// :nodoc: Protocol requirement. + public var body: some View { + content + } + + /// :nodoc: Protocol requirement. + public func canDisplay(_ element: AnyWorkflow.Element?) -> Bool { + content.canDisplay(element) + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension EitherWorkflowItem { + /// :nodoc: Protocol requirement. + public func modify(workflow: AnyWorkflow) { + content.modify(workflow: workflow) + } + + /// :nodoc: Protocol requirement. + public var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { + content.workflowLaunchStyle + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/Views/OptionalWorkflowItem.swift b/Sources/SwiftCurrent_SwiftUI/Views/OptionalWorkflowItem.swift new file mode 100644 index 000000000..a9b6fdc3c --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Views/OptionalWorkflowItem.swift @@ -0,0 +1,46 @@ +// +// OptionalWorkflowItem.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 3/17/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +/// :nodoc: ResultBuilder requirement. +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public struct OptionalWorkflowItem: View, _WorkflowItemProtocol { + /// :nodoc: Protocol requirement. + public typealias FlowRepresentableType = WI.FlowRepresentableType + + @State var content: WI? + + /// :nodoc: Protocol requirement. + public var body: some View { + content + } + + init(content: WI?) { + _content = State(initialValue: content) + } + + /// :nodoc: Protocol requirement. + public func canDisplay(_ element: AnyWorkflow.Element?) -> Bool { + content?.canDisplay(element) ?? false + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension OptionalWorkflowItem { + /// :nodoc: Protocol requirement. + public func modify(workflow: AnyWorkflow) { + content?.modify(workflow: workflow) + } + + /// :nodoc: Protocol requirement. + public var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { + content?.workflowLaunchStyle ?? .default + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowGroup.swift b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowGroup.swift new file mode 100644 index 000000000..fc1ec8787 --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowGroup.swift @@ -0,0 +1,42 @@ +// +// File.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 3/8/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public struct WorkflowGroup: View, _WorkflowItemProtocol { + public typealias FlowRepresentableType = WI.FlowRepresentableType + + @State var content: WI + + public var body: some View { + content + } + + public init(@WorkflowBuilder content: () -> WI) { + _content = State(initialValue: content()) + } + + public func canDisplay(_ element: AnyWorkflow.Element?) -> Bool { + content.canDisplay(element) + } +} + +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension WorkflowGroup { + /// :nodoc: Protocol requirement. + public func modify(workflow: AnyWorkflow) { + content.modify(workflow: workflow) + } + + /// :nodoc: Protocol requirement. + public var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { + content.workflowLaunchStyle + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift index e18179899..496c0eb14 100644 --- a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift +++ b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItem.swift @@ -14,189 +14,123 @@ import UIKit /** A concrete type used to modify a `FlowRepresentable` in a workflow. + ### Discussion - `WorkflowItem` gives you the ability to specify changes you'd like to apply to a specific `FlowRepresentable` when it is time to present it in a `Workflow`. You create `WorkflowItem`s by calling a `thenProceed` method, e.g. `View.thenProceed(with:)`, inside of a `WorkflowLauncher`. + `WorkflowItem` gives you the ability to specify changes you'd like to apply to a specific `FlowRepresentable` when it is time to present it in a `Workflow`. `WorkflowItem`s are most often created inside a `WorkflowView` or `WorkflowGroup`. + #### Example ```swift - thenProceed(FirstView.self) - .persistence(.removedAfterProceeding) // affects only FirstView - .applyModifiers { - $0.background(Color.gray) // $0 is a FirstView instance - .transition(.slide) - .animation(.spring()) - } + WorkflowItem(FirstView.self) + .persistence(.removedAfterProceeding) // affects only FirstView + .applyModifiers { + $0.background(Color.gray) // $0 is a FirstView instance + .transition(.slide) + .animation(.spring()) + } ``` */ @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -public struct WorkflowItem: View { +public struct WorkflowItem: _WorkflowItemProtocol { // swiftlint:disable:this generic_type_name // These need to be state variables to survive SwiftUI re-rendering. Change under penalty of torture BY the codebase you modified. @State private var content: Content? - @State private var wrapped: Wrapped? @State private var metadata: FlowRepresentableMetadata! @State private var modifierClosure: ((AnyFlowRepresentableView) -> Void)? @State private var flowPersistenceClosure: (AnyWorkflow.PassedArgs) -> FlowPersistence = { _ in .default } @State private var launchStyle: LaunchStyle.SwiftUI.PresentationType = .default @State private var persistence: FlowPersistence = .default - @State private var elementRef: AnyWorkflow.Element? - @State private var isActive = false @EnvironmentObject private var model: WorkflowViewModel - @EnvironmentObject private var launcher: Launcher - @Environment(\.presentationMode) var presentation - let inspection = Inspection() + private var elementRef: AnyWorkflow.Element? public var body: some View { ViewBuilder { - if launchStyle == .navigationLink, let content = content { - content.navLink(to: nextView, isActive: $isActive) - } else if case .modal(let modalStyle) = (wrapped as? WorkflowItemPresentable)?.workflowLaunchStyle, let content = content { - switch modalStyle { - case .sheet: content.testableSheet(isPresented: $isActive) { nextView } - #if (os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)) - case .fullScreenCover: content.fullScreenCover(isPresented: $isActive) { nextView } - #endif - } - } else if let body = model.body?.extractErasedView() as? Content, elementRef == nil || elementRef === model.body, launchStyle != .navigationLink { - content ?? body - } else { - nextView + content ?? model.body?.extractErasedView() as? Content + } + .onReceive(model.$body) { + if let body = $0?.extractErasedView() as? Content, elementRef == nil || elementRef === $0 { + content = body } } - .onReceive(model.$body, perform: activateIfNeeded) - .onReceive(model.$body, perform: proceedInWorkflow) - .onReceive(model.onBackUpPublisher, perform: backUpInWorkflow) - .onReceive(inspection.notice) { inspection.visit(self, $0) } } - @ViewBuilder private var nextView: some View { - wrapped?.environmentObject(model).environmentObject(launcher) + public func canDisplay(_ element: AnyWorkflow.Element?) -> Bool { + (element?.extractErasedView() as? Content != nil) && (elementRef == nil || elementRef === element) } - private init(previous: WorkflowItem, _ closure: () -> Wrapped) where Wrapped == WorkflowItem { - let wrapped = closure() - _wrapped = State(initialValue: wrapped) - _metadata = previous._metadata - _modifierClosure = previous._modifierClosure - _flowPersistenceClosure = previous._flowPersistenceClosure - _launchStyle = previous._launchStyle + public mutating func setElementRef(_ element: AnyWorkflow.Element?) { + if canDisplay(element) { + elementRef = element + } } - private init(previous: WorkflowItem, + private init(previous: WorkflowItem, launchStyle: LaunchStyle.SwiftUI.PresentationType, modifierClosure: @escaping ((AnyFlowRepresentableView) -> Void), flowPersistenceClosure: @escaping (AnyWorkflow.PassedArgs) -> FlowPersistence) { - _wrapped = previous._wrapped _modifierClosure = State(initialValue: modifierClosure) _flowPersistenceClosure = State(initialValue: flowPersistenceClosure) _launchStyle = State(initialValue: launchStyle) - let metadata = FlowRepresentableMetadata(F.self, + let metadata = FlowRepresentableMetadata(FlowRepresentableType.self, launchStyle: launchStyle.rawValue, flowPersistence: flowPersistenceClosure, flowRepresentableFactory: factory) _metadata = State(initialValue: metadata) } - init(_ item: F.Type) where Wrapped == Never, Content == F { - let metadata = FlowRepresentableMetadata(Content.self, - launchStyle: .new, - flowPersistence: flowPersistenceClosure, - flowRepresentableFactory: factory) - _metadata = State(initialValue: metadata) - } - - init(_ item: F.Type, wrapped: () -> Wrapped) where Content == F { - let metadata = FlowRepresentableMetadata(Content.self, - launchStyle: .new, - flowPersistence: flowPersistenceClosure, - flowRepresentableFactory: factory) - _metadata = State(initialValue: metadata) - _wrapped = State(initialValue: wrapped()) - } - - #if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)) && canImport(UIKit) - /// Creates a `WorkflowItem` from a `UIViewController`. - @available(iOS 14.0, macOS 11, tvOS 14.0, *) - init(_: VC.Type) where Content == ViewControllerWrapper, Wrapped == Never, F == ViewControllerWrapper { - let metadata = FlowRepresentableMetadata(ViewControllerWrapper.self, + /// Creates a workflow item from a FlowRepresentable type + public init(_ item: FlowRepresentableType.Type) where Content == FlowRepresentableType { + let metadata = FlowRepresentableMetadata(FlowRepresentableType.self, launchStyle: .new, flowPersistence: flowPersistenceClosure, flowRepresentableFactory: factory) _metadata = State(initialValue: metadata) } +#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)) && canImport(UIKit) /// Creates a `WorkflowItem` from a `UIViewController`. @available(iOS 14.0, macOS 11, tvOS 14.0, *) - init(_: VC.Type, wrapped: () -> Wrapped) where Content == ViewControllerWrapper, F == ViewControllerWrapper { - let wrapped = wrapped() - _wrapped = State(initialValue: wrapped) + public init(_: VC.Type) where Content == ViewControllerWrapper, FlowRepresentableType == ViewControllerWrapper { let metadata = FlowRepresentableMetadata(ViewControllerWrapper.self, launchStyle: .new, flowPersistence: flowPersistenceClosure, flowRepresentableFactory: factory) _metadata = State(initialValue: metadata) } - #endif +#endif /** Provides a way to apply modifiers to your `FlowRepresentable` view. ### Important: The most recently defined (or last) use of this, is the only one that applies modifiers, unlike onAbandon or onFinish. */ - public func applyModifiers(@ViewBuilder _ closure: @escaping (F) -> V) -> WorkflowItem { - WorkflowItem(previous: self, - launchStyle: launchStyle, - modifierClosure: { - // We are essentially casting this to itself, that cannot fail. (Famous last words) - // swiftlint:disable:next force_cast - let instance = $0.underlyingInstance as! F - $0.changeUnderlyingView(to: closure(instance)) - }, - flowPersistenceClosure: flowPersistenceClosure) + public func applyModifiers(@ViewBuilder _ closure: @escaping (FlowRepresentableType) -> V) -> WorkflowItem { + WorkflowItem(previous: self, + launchStyle: launchStyle, + modifierClosure: { + // We are essentially casting this to itself, that cannot fail. (Famous last words) + // swiftlint:disable:next force_cast + let instance = $0.underlyingInstance as! FlowRepresentableType + $0.changeUnderlyingView(to: closure(instance)) + }, + flowPersistenceClosure: flowPersistenceClosure) } private func factory(args: AnyWorkflow.PassedArgs) -> AnyFlowRepresentable { - let afrv = AnyFlowRepresentableView(type: F.self, args: args) + let afrv = AnyFlowRepresentableView(type: FlowRepresentableType.self, args: args) modifierClosure?(afrv) return afrv } - - private func activateIfNeeded(element: AnyWorkflow.Element?) { - if elementRef != nil, elementRef === element?.previouslyLoadedElement { - isActive = true - } - } - - private func backUpInWorkflow(element: AnyWorkflow.Element?) { - // We have found no satisfactory way to test this...we haven't even really found unsatisfactory ways to test it. - // See: https://github.com/nalexn/ViewInspector/issues/131 - if elementRef === element { - presentation.wrappedValue.dismiss() - } - } - - private func proceedInWorkflow(element: AnyWorkflow.Element?) { - if let body = element?.extractErasedView() as? Content, elementRef === element || elementRef == nil { - elementRef = element - content = body - persistence = element?.value.metadata.persistence ?? .default - } else if persistence == .removedAfterProceeding { - content = nil - elementRef = nil - } - } } @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -extension WorkflowItem: WorkflowItemPresentable { - var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { +extension WorkflowItem { + /// :nodoc: Protocol requirement. + public var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { launchStyle } -} -@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -extension WorkflowItem: WorkflowModifier { - func modify(workflow: AnyWorkflow) { + /// :nodoc: Protocol requirement. + public func modify(workflow: AnyWorkflow) { workflow.append(metadata) - (wrapped as? WorkflowModifier)?.modify(workflow: workflow) } } @@ -212,20 +146,20 @@ extension WorkflowItem { } /// Sets persistence on the `FlowRepresentable` of the `WorkflowItem`. - public func persistence(_ persistence: @escaping (F.WorkflowInput) -> FlowPersistence.SwiftUI.Persistence) -> Self { + public func persistence(_ persistence: @escaping (FlowRepresentableType.WorkflowInput) -> FlowPersistence.SwiftUI.Persistence) -> Self { Self(previous: self, launchStyle: launchStyle, modifierClosure: modifierClosure ?? { _ in }, flowPersistenceClosure: { - guard case .args(let arg as F.WorkflowInput) = $0 else { - fatalError("Could not cast \(String(describing: $0)) to expected type: \(F.WorkflowInput.self)") - } + guard case .args(let arg as FlowRepresentableType.WorkflowInput) = $0 else { + fatalError("Could not cast \(String(describing: $0)) to expected type: \(FlowRepresentableType.WorkflowInput.self)") + } return persistence(arg).rawValue - }) + }) } /// Sets persistence on the `FlowRepresentable` of the `WorkflowItem`. - public func persistence(_ persistence: @escaping (F.WorkflowInput) -> FlowPersistence.SwiftUI.Persistence) -> Self where F.WorkflowInput == AnyWorkflow.PassedArgs { + public func persistence(_ persistence: @escaping (FlowRepresentableType.WorkflowInput) -> FlowPersistence.SwiftUI.Persistence) -> Self where FlowRepresentableType.WorkflowInput == AnyWorkflow.PassedArgs { // swiftlint:disable:this line_length Self(previous: self, launchStyle: launchStyle, modifierClosure: modifierClosure ?? { _ in }, @@ -233,7 +167,7 @@ extension WorkflowItem { } /// Sets persistence on the `FlowRepresentable` of the `WorkflowItem`. - public func persistence(_ persistence: @escaping () -> FlowPersistence.SwiftUI.Persistence) -> Self where F.WorkflowInput == Never { + public func persistence(_ persistence: @escaping () -> FlowPersistence.SwiftUI.Persistence) -> Self where FlowRepresentableType.WorkflowInput == Never { Self(previous: self, launchStyle: launchStyle, modifierClosure: modifierClosure ?? { _ in }, diff --git a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItemWrapper.swift b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItemWrapper.swift new file mode 100644 index 000000000..81fa46a47 --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowItemWrapper.swift @@ -0,0 +1,111 @@ +// +// WorkflowItemWrapper.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 3/8/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +/// :nodoc: ResultBuilder requirement. +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public struct WorkflowItemWrapper: View, _WorkflowItemProtocol { + public typealias FlowRepresentableType = WI.FlowRepresentableType + + @State private var content: WI + @State private var wrapped: Wrapped? + @State private var elementRef: AnyWorkflow.Element? + @State private var isActive = false + @EnvironmentObject private var model: WorkflowViewModel + @EnvironmentObject private var launcher: Launcher + @Environment(\.presentationMode) var presentation + + let inspection = Inspection() + + var launchStyle: LaunchStyle.SwiftUI.PresentationType { + content.workflowLaunchStyle + } + + /// :nodoc: Protocol requirement. + public var workflowLaunchStyle: LaunchStyle.SwiftUI.PresentationType { + content.workflowLaunchStyle + } + + public var body: some View { + ViewBuilder { + if launchStyle == .navigationLink { + content.navLink(to: nextView, isActive: $isActive) + } else if case .modal(let modalStyle) = wrapped?.workflowLaunchStyle { + content.modal(isPresented: $isActive, style: modalStyle, destination: nextView) + } else if launchStyle != .navigationLink, content.canDisplay(model.body) { + content + } else { + nextView + } + } + .onReceive(model.$body, perform: activateIfNeeded) + .onReceive(model.$body, perform: proceedInWorkflow) + .onReceive(model.onBackUpPublisher, perform: backUpInWorkflow) + .onReceive(inspection.notice) { inspection.visit(self, $0) } + } + + @ViewBuilder private var nextView: some View { + wrapped?.environmentObject(model).environmentObject(launcher) + } + + init(content: WI) where Wrapped == Never { + _wrapped = State(initialValue: nil) + _elementRef = State(initialValue: nil) + _content = State(initialValue: content) + } + + init(content: WI, wrapped: () -> Wrapped) { + _wrapped = State(initialValue: wrapped()) + _elementRef = State(initialValue: nil) + _content = State(initialValue: content) + // This may no longer be necessary depending on: https://forums.swift.org/t/pitch-buildpartialblock-for-result-builders/55561 + verifyWorkflowIsWellFormed(WI.FlowRepresentableType.self, Wrapped.FlowRepresentableType.self) + } + + private func verifyWorkflowIsWellFormed(_ lhs: LHS.Type, _ rhs: RHS.Type) { + guard !(RHS.WorkflowInput.self is Never.Type), // an input type of `Never` indicates any arguments will simply be ignored + !(RHS.WorkflowInput.self is AnyWorkflow.PassedArgs.Type), // an input type of `AnyWorkflow.PassedArgs` means either some value or no value can be passed + !(LHS.WorkflowOutput.self is AnyWorkflow.PassedArgs.Type) else { return } // an output type of `AnyWorkflow.PassedArgs` can only be checked at runtime when the actual value is passed forward + + // trap if workflow is malformed (output does not match input) + // swiftlint:disable:next line_length + assert(LHS.WorkflowOutput.self is RHS.WorkflowInput.Type, "Workflow is malformed, expected output of: \(LHS.self) (\(LHS.WorkflowOutput.self)) to match input of: \(RHS.self) (\(RHS.WorkflowInput.self)") + } + + public func canDisplay(_ element: AnyWorkflow.Element?) -> Bool { + content.canDisplay(element) || wrapped?.canDisplay(element) == true + } + + public func modify(workflow: AnyWorkflow) { + content.modify(workflow: workflow) + wrapped?.modify(workflow: workflow) + } + + private func activateIfNeeded(element: AnyWorkflow.Element?) { + if elementRef != nil, elementRef === element?.previouslyLoadedElement { + isActive = true + } + } + + private func backUpInWorkflow(element: AnyWorkflow.Element?) { + // We have found no satisfactory way to test this...we haven't even really found unsatisfactory ways to test it. + // See: https://github.com/nalexn/ViewInspector/issues/131 + if elementRef === element { + presentation.wrappedValue.dismiss() + } + } + + private func proceedInWorkflow(element: AnyWorkflow.Element?) { + if content.canDisplay(element), elementRef === element || elementRef == nil { + elementRef = element + } + content.setElementRef(element) + } +} diff --git a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift index e01b40a95..29e0f114f 100644 --- a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift +++ b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowLauncher.swift @@ -9,39 +9,11 @@ import SwiftUI import SwiftCurrent -/** - Used to build a `Workflow` in SwiftUI; call thenProceed to create a SwiftUI view. - - ### Discussion - The preferred method for creating a `Workflow` with SwiftUI is a combination of `WorkflowLauncher` and `WorkflowItem`. Initialize with arguments if your first `FlowRepresentable` has an input type. - - #### Example - ```swift - WorkflowLauncher(isLaunched: $isLaunched.animation(), args: "String in") { - thenProceed(with: FirstView.self) { - thenProceed(with: SecondView.self) - .persistence(.removedAfterProceeding) - .applyModifiers { - $0.SecondViewSpecificModifier() - .padding(10) - .background(Color.purple) - .transition(.opacity) - .animation(.easeInOut) - } - }.applyModifiers { - $0.background(Color.gray) - .transition(.slide) - .animation(.spring()) - } - } - .onAbandon { print("isLaunched is now false") } - .onFinish { args in print("Finished 1: \(args)") } - .onFinish { print("Finished 2: \($0)") } - .background(Color.green) - ``` - */ +/// :nodoc: WorkflowView requirement. @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -public struct WorkflowLauncher: View { +public struct WorkflowLauncher: View { + public typealias WorkflowInput = Content.FlowRepresentableType.WorkflowInput + @State private var content: Content @State private var onFinish = [(AnyWorkflow.PassedArgs) -> Void]() @State private var onAbandon = [() -> Void]() @@ -77,65 +49,10 @@ 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 content: closure that holds the `WorkflowItem` - */ - public init(isLaunched: Binding, content: () -> Content) where Content == WorkflowItem, F.WorkflowInput == Never { - self.init(isLaunched: isLaunched, startingArgs: .none, content: content()) - } - - /** - 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 content: closure that holds the `WorkflowItem` - */ - public init(isLaunched: Binding, startingArgs: A, content: () -> Content) where Content == WorkflowItem, F.WorkflowInput == Never { - self.init(isLaunched: isLaunched, startingArgs: .args(startingArgs), content: content()) - } - - /** - 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 content: closure that holds the `WorkflowItem` - */ - public init(isLaunched: Binding, startingArgs: F.WorkflowInput, content: () -> Content) where Content == WorkflowItem { - self.init(isLaunched: isLaunched, startingArgs: .args(startingArgs), content: content()) - } - - /** - 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 content: closure that holds the `WorkflowItem` - */ - public init(isLaunched: Binding, startingArgs: F.WorkflowInput = .none, content: () -> Content) where Content == WorkflowItem, F.WorkflowInput == AnyWorkflow.PassedArgs { - self.init(isLaunched: isLaunched, startingArgs: startingArgs, content: content()) - } - - /** - 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 content: closure that holds the `WorkflowItem` - */ - public init(isLaunched: Binding, startingArgs: AnyWorkflow.PassedArgs, content: () -> Content) where Content == WorkflowItem { + init(isLaunched: Binding, startingArgs: AnyWorkflow.PassedArgs, content: () -> Content) { self.init(isLaunched: isLaunched, startingArgs: startingArgs, content: content()) } - /** - 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 content: closure that holds the `WorkflowItem` - */ - public init(isLaunched: Binding, startingArgs: A, content: () -> Content) where Content == WorkflowItem, F.WorkflowInput == AnyWorkflow.PassedArgs { - self.init(isLaunched: isLaunched, startingArgs: .args(startingArgs), content: content()) - } - private init(current: Self, shouldEmbedInNavView: Bool, onFinish: [(AnyWorkflow.PassedArgs) -> Void], onAbandon: [() -> Void]) { _model = current._model _launcher = current._launcher @@ -146,7 +63,7 @@ public struct WorkflowLauncher: View { _onAbandon = State(initialValue: onAbandon) } - private init(isLaunched: Binding, startingArgs: AnyWorkflow.PassedArgs, content: Content) where Content == WorkflowItem { + private init(isLaunched: Binding, startingArgs: AnyWorkflow.PassedArgs, content: Content) { _isLaunched = isLaunched let wf = AnyWorkflow.empty content.modify(workflow: wf) @@ -167,22 +84,19 @@ public struct WorkflowLauncher: View { onFinish.forEach { $0(args) } } - /// Adds an action to perform when this `Workflow` has finished. - public func onFinish(closure: @escaping (AnyWorkflow.PassedArgs) -> Void) -> Self { + func onFinish(closure: @escaping (AnyWorkflow.PassedArgs) -> Void) -> Self { var onFinish = self.onFinish onFinish.append(closure) return Self(current: self, shouldEmbedInNavView: shouldEmbedInNavView, onFinish: onFinish, onAbandon: onAbandon) } - /// Adds an action to perform when this `Workflow` has abandoned. - public func onAbandon(closure: @escaping () -> Void) -> Self { + func onAbandon(closure: @escaping () -> Void) -> Self { var onAbandon = self.onAbandon onAbandon.append(closure) return Self(current: self, shouldEmbedInNavView: shouldEmbedInNavView, onFinish: onFinish, onAbandon: onAbandon) } - /// Wraps content in a NavigationView. - public func embedInNavigationView() -> Self { + func embedInNavigationView() -> Self { Self(current: self, shouldEmbedInNavView: true, onFinish: onFinish, onAbandon: onAbandon) } } diff --git a/Sources/SwiftCurrent_SwiftUI/Views/WorkflowView.swift b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowView.swift new file mode 100644 index 000000000..ac31af069 --- /dev/null +++ b/Sources/SwiftCurrent_SwiftUI/Views/WorkflowView.swift @@ -0,0 +1,159 @@ +// +// WorkflowView.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 2/21/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import SwiftUI +import SwiftCurrent + +/** + Used to build a `Workflow` in SwiftUI; Embed `WorkflowItem`s in a `WorkflowView` to create a SwiftUI view. + + ### Discussion + The preferred method for creating a `Workflow` with SwiftUI is a combination of `WorkflowView` and `WorkflowItem`. Initialize with arguments if your first `FlowRepresentable` has an input type. + + #### Example + ```swift + WorkflowView(isLaunched: $isLaunched.animation(), launchingWith: "String in") { + WorkflowItem(FirstView.self) + .applyModifiers { + $0.background(Color.gray) + .transition(.slide) + .animation(.spring()) + } + WorkflowItem(SecondView.self) + .persistence(.removedAfterProceeding) + .applyModifiers { + $0.SecondViewSpecificModifier() + .padding(10) + .background(Color.purple) + .transition(.opacity) + .animation(.easeInOut) + } + } + .onAbandon { print("isLaunched is now false") } + .onFinish { args in print("Finished 1: \(args)") } + .onFinish { print("Finished 2: \($0)") } + .background(Color.green) + ``` + */ +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +public struct WorkflowView: View { + @State var content: Content + + let inspection = Inspection() + + public var body: some View { + content + .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 content: `WorkflowBuilder` consisting of `WorkflowItem`s that define your workflow. + */ + public init(isLaunched: Binding = .constant(true), + @WorkflowBuilder content: () -> WI) where Content == WorkflowLauncher, WI.FlowRepresentableType.WorkflowInput == Never { + self.init(isLaunched: isLaunched, startingArgs: .none, content: content()) + } + + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter launchingWith: arguments passed to the first loaded `FlowRepresentable` in the underlying `Workflow`. + - Parameter content: `WorkflowBuilder` consisting of `WorkflowItem`s that define your workflow. + */ + public init(isLaunched: Binding = .constant(true), + launchingWith args: WI.FlowRepresentableType.WorkflowInput, + @WorkflowBuilder content: () -> WI) where Content == WorkflowLauncher { + self.init(isLaunched: isLaunched, startingArgs: .args(args), content: content()) + } + + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter launchingWith: arguments passed to the first loaded `FlowRepresentable` in the underlying `Workflow`. + - Parameter content: `WorkflowBuilder` consisting of `WorkflowItem`s that define your workflow. + */ + public init(isLaunched: Binding = .constant(true), + launchingWith args: AnyWorkflow.PassedArgs, + @WorkflowBuilder content: () -> WI) where Content == WorkflowLauncher, WI.FlowRepresentableType.WorkflowInput == AnyWorkflow.PassedArgs { + self.init(isLaunched: isLaunched, startingArgs: args, content: content()) + } + + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter launchingWith: arguments passed to the first loaded `FlowRepresentable` in the underlying `Workflow`. + - Parameter content: `WorkflowBuilder` consisting of `WorkflowItem`s that define your workflow. + */ + public init(isLaunched: Binding = .constant(true), + launchingWith args: AnyWorkflow.PassedArgs, + @WorkflowBuilder content: () -> WI) where Content == WorkflowLauncher { + self.init(isLaunched: isLaunched, startingArgs: args, content: content()) + } + + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter launchingWith: arguments passed to the first loaded `FlowRepresentable` in the underlying `Workflow`. + - Parameter content: `WorkflowBuilder` consisting of `WorkflowItem`s that define your workflow. + */ + public init(isLaunched: Binding = .constant(true), + launchingWith args: A, + @WorkflowBuilder content: () -> WI) where Content == WorkflowLauncher, WI.FlowRepresentableType.WorkflowInput == AnyWorkflow.PassedArgs { + self.init(isLaunched: isLaunched, startingArgs: .args(args), content: content()) + } + + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter launchingWith: arguments passed to the first loaded `FlowRepresentable` in the underlying `Workflow`. + - Parameter content: `WorkflowBuilder` consisting of `WorkflowItem`s that define your workflow. + */ + public init(isLaunched: Binding = .constant(true), + launchingWith args: A, + @WorkflowBuilder content: () -> WI) where Content == WorkflowLauncher, WI.FlowRepresentableType.WorkflowInput == Never { + self.init(isLaunched: isLaunched, startingArgs: .args(args), content: content()) + } + + /** + Creates a base for proceeding with a `WorkflowItem`. + - Parameter isLaunched: binding that controls launching the underlying `Workflow`. + - Parameter content: `WorkflowBuilder` consisting of `WorkflowItem`s that define your workflow. + */ + public init(isLaunched: Binding = .constant(true), + @WorkflowBuilder content: () -> WI) where Content == WorkflowLauncher, WI.FlowRepresentableType.WorkflowInput == AnyWorkflow.PassedArgs { + self.init(isLaunched: isLaunched, startingArgs: .none, content: content()) + } + + private init(isLaunched: Binding, + startingArgs: AnyWorkflow.PassedArgs, + content: WI) where Content == WorkflowLauncher { + _content = State(wrappedValue: WorkflowLauncher(isLaunched: isLaunched, startingArgs: startingArgs) { content }) + } + + private init(_ other: WorkflowView, + newContent: Content) where Content == WorkflowLauncher { + _content = State(wrappedValue: newContent) + } + + /// Adds an action to perform when this `Workflow` has finished. + public func onFinish(_ closure: @escaping (AnyWorkflow.PassedArgs) -> Void) -> Self where Content == WorkflowLauncher { + Self(self, newContent: _content.wrappedValue.onFinish(closure: closure)) + } + + /// Adds an action to perform when this `Workflow` has abandoned. + public func onAbandon(_ closure: @escaping () -> Void) -> Self where Content == WorkflowLauncher { + Self(self, newContent: _content.wrappedValue.onAbandon(closure: closure)) + } + + /// Wraps content in a NavigationView. + public func embedInNavigationView() -> Self where Content == WorkflowLauncher { + Self(self, newContent: _content.wrappedValue.embedInNavigationView()) + } +} diff --git a/SwiftCurrent.podspec b/SwiftCurrent.podspec index f4b125307..4a5b50a31 100644 --- a/SwiftCurrent.podspec +++ b/SwiftCurrent.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SwiftCurrent' - s.version = '4.5.24' + s.version = '5.0.0' s.summary = 'A library for complex workflows in Swift' s.description = <<-DESC SwiftCurrent is a library that lets you easily manage journeys through your Swift application. diff --git a/Tests/SwiftCurrent_SwiftUITests/GenericConstraintTests.swift b/Tests/SwiftCurrent_SwiftUITests/GenericConstraintTests.swift index 989cb32a4..9209bb8ff 100644 --- a/Tests/SwiftCurrent_SwiftUITests/GenericConstraintTests.swift +++ b/Tests/SwiftCurrent_SwiftUITests/GenericConstraintTests.swift @@ -44,10 +44,10 @@ final class GenericConstraintTests: XCTestCase, View { } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: Optional("Discarded arguments")) { - thenProceed(with: FR1.self) + WorkflowView(launchingWith: Optional("Discarded arguments")) { + WorkflowItem(FR1.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection) XCTAssertNoThrow(try workflowView.find(FR1.self)) } @@ -68,12 +68,11 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgument = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgument) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgument) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection) XCTAssertEqual(try workflowView.find(FR2.self).actualView().input, expectedArgument) } @@ -85,10 +84,10 @@ final class GenericConstraintTests: XCTestCase, View { } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self).persistence(.removedAfterProceeding) + WorkflowView { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) } @@ -100,10 +99,10 @@ final class GenericConstraintTests: XCTestCase, View { } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self).presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().presentationType, .navigationLink) } @@ -115,13 +114,13 @@ final class GenericConstraintTests: XCTestCase, View { } let expectation = self.expectation(description: "FlowPersistence closure called") let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self).persistence { + WorkflowView { + WorkflowItem(FR1.self).persistence { defer { expectation.fulfill() } return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) wait(for: [expectation], timeout: TestConstant.timeout) @@ -137,12 +136,11 @@ final class GenericConstraintTests: XCTestCase, View { var body: some View { Text(String(describing: Self.self)) } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow() let view = try await workflowView.actualView().getWrappedView() XCTAssertNoThrow(try workflowView.find(type(of: view)).find(FR2.self)) @@ -158,16 +156,15 @@ final class GenericConstraintTests: XCTestCase, View { var body: some View { Text(String(describing: Self.self)) } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -181,16 +178,15 @@ final class GenericConstraintTests: XCTestCase, View { var body: some View { Text(String(describing: Self.self)) } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { .removedAfterProceeding } + WorkflowView { + WorkflowItem(FR1.self).persistence { .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -205,15 +201,14 @@ final class GenericConstraintTests: XCTestCase, View { init(with args: AnyWorkflow.PassedArgs) { } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -228,16 +223,15 @@ final class GenericConstraintTests: XCTestCase, View { init(with args: AnyWorkflow.PassedArgs) { } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -252,16 +246,15 @@ final class GenericConstraintTests: XCTestCase, View { init(with args: AnyWorkflow.PassedArgs) { } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { .removedAfterProceeding } + WorkflowView { + WorkflowItem(FR1.self).persistence { .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -277,15 +270,14 @@ final class GenericConstraintTests: XCTestCase, View { init(with args: Int) { } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -301,16 +293,15 @@ final class GenericConstraintTests: XCTestCase, View { init(with args: Int) { } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -326,16 +317,15 @@ final class GenericConstraintTests: XCTestCase, View { init(with args: Int) { } } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { .removedAfterProceeding } + WorkflowView { + WorkflowItem(FR1.self).persistence { .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -350,10 +340,10 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self).persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) } @@ -367,10 +357,10 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self).presentationType(.navigationLink) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).presentationType(.navigationLink) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().presentationType, .navigationLink) } @@ -385,14 +375,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectation = self.expectation(description: "FlowPersistence closure called") let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self).persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) defer { expectation.fulfill() } return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) wait(for: [expectation], timeout: TestConstant.timeout) @@ -411,15 +401,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -436,16 +425,15 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -462,19 +450,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -492,15 +479,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -518,16 +504,15 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -545,19 +530,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -576,15 +560,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -603,16 +586,15 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -631,19 +613,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -657,10 +638,10 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self).persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) } @@ -674,10 +655,10 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self).presentationType(.navigationLink) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).presentationType(.navigationLink) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().presentationType, .navigationLink) } @@ -692,14 +673,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectation = self.expectation(description: "FlowPersistence closure called") let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self).persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0, expectedArgs) defer { expectation.fulfill() } return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) wait(for: [expectation], timeout: TestConstant.timeout) @@ -718,15 +699,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -743,16 +723,15 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -769,19 +748,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0, expectedArgs) return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -799,15 +777,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -825,16 +802,15 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -852,19 +828,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0, expectedArgs) return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -883,15 +858,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -910,16 +884,15 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -938,19 +911,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0, expectedArgs) return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -969,15 +941,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR1.self).proceedInWorkflow("") - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -996,16 +967,15 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence(.removedAfterProceeding) + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow("") - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -1024,19 +994,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR1.self).persistence { XCTAssertEqual($0, expectedArgs) return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await workflowView.find(FR1.self).proceedInWorkflow("") - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -1056,15 +1025,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self).persistence(.removedAfterProceeding) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertEqual(try view.find(FR1.self).actualView().persistence, .removedAfterProceeding) } @@ -1081,18 +1049,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectation = self.expectation(description: "FlowPersistence closure called") let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self).persistence { - defer { expectation.fulfill() } - return .removedAfterProceeding - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence { + defer { expectation.fulfill() } + return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertEqual(try view.find(FR1.self).actualView().persistence, .removedAfterProceeding) wait(for: [expectation], timeout: TestConstant.timeout) } @@ -1113,21 +1080,19 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -1147,19 +1112,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) XCTAssertEqual(try view.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1180,19 +1143,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { .removedAfterProceeding } - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) XCTAssertEqual(try view.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1214,19 +1175,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -1247,19 +1206,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) XCTAssertEqual(try view.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1281,20 +1238,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { _ in .removedAfterProceeding } - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { _ in .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) XCTAssertEqual(try view.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1317,19 +1272,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -1351,19 +1304,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) XCTAssertEqual(try view.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1386,22 +1337,20 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { - XCTAssertEqual($0, 1) - return .removedAfterProceeding - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { + XCTAssertEqual($0, 1) + return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow(1) - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) XCTAssertEqual(try view.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1422,12 +1371,11 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self).persistence(.removedAfterProceeding) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() XCTAssertEqual(try workflowView.find(FR1.self).actualView().persistence, .removedAfterProceeding) @@ -1447,19 +1395,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectation = self.expectation(description: "FlowPersistence closure called") let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self).persistence { - XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) - defer { expectation.fulfill() } - return .removedAfterProceeding - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence { + XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) + defer { expectation.fulfill() } + return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertEqual(try view.find(FR1.self).actualView().persistence, .removedAfterProceeding) wait(for: [expectation], timeout: TestConstant.timeout) } @@ -1481,19 +1428,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) } @@ -1514,19 +1459,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let workflowItem = try await workflowView.extractWrappedWorkflowItem() + let workflowItem = try await workflowView.extractWrappedWrapper() try await workflowItem.find(FR1.self).proceedInWorkflow() - let view = try await workflowItem.extractWrappedWorkflowItem() + let view = try await workflowItem.extractWrappedWrapper() XCTAssertNoThrow(try view.find(FR2.self)) XCTAssertEqual(try view.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1548,23 +1491,21 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.persistence { - XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) - return .removedAfterProceeding - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence { + XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) + return .removedAfterProceeding } + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() XCTAssertEqual(try wfr1.find(FR1.self).actualView().persistence, .removedAfterProceeding) try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) } @@ -1586,19 +1527,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) } @@ -1620,19 +1559,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1655,22 +1592,20 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { - XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) - return .removedAfterProceeding - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { + XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedArgs) + return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1694,19 +1629,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(1) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) } @@ -1729,19 +1662,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(1) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1765,22 +1696,20 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { - XCTAssertEqual($0, 1) - return .removedAfterProceeding - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { + XCTAssertEqual($0, 1) + return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(1) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1799,10 +1728,9 @@ final class GenericConstraintTests: XCTestCase, View { } try XCTAssertThrowsFatalError { - _ = WorkflowLauncher(isLaunched: .constant(true)) { - self.thenProceed(with: FR0.self) { - self.thenProceed(with: FR1.self).persistence(.removedAfterProceeding) - } + _ = WorkflowView { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) } } } @@ -1820,15 +1748,14 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self).persistence(.removedAfterProceeding) - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertEqual(try view.find(FR1.self).actualView().persistence, .removedAfterProceeding) } @@ -1846,19 +1773,18 @@ final class GenericConstraintTests: XCTestCase, View { let expectation = self.expectation(description: "FlowPersistence closure called") let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self).persistence { - XCTAssertEqual($0, expectedArgs) - defer { expectation.fulfill() } - return .removedAfterProceeding - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self).persistence { + XCTAssertEqual($0, expectedArgs) + defer { expectation.fulfill() } + return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let view = try await workflowView.extractWrappedWorkflowItem() + let view = try await workflowView.extractWrappedWrapper() XCTAssertEqual(try view.find(FR1.self).actualView().persistence, .removedAfterProceeding) wait(for: [expectation], timeout: TestConstant.timeout) } @@ -1880,19 +1806,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) } @@ -1913,19 +1837,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1947,19 +1869,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { .removedAfterProceeding } - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -1982,19 +1902,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) } @@ -2016,19 +1934,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2051,19 +1967,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { _ in .removedAfterProceeding } - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { _ in .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2087,19 +2001,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(1) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) } @@ -2122,19 +2034,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(1) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2158,22 +2068,20 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { - XCTAssertEqual($0, 1) - return .removedAfterProceeding - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { + XCTAssertEqual($0, 1) + return .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(1) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2197,19 +2105,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow("") - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) } @@ -2232,19 +2138,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow("") - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2268,19 +2172,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence { _ in .removedAfterProceeding } - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence { _ in .removedAfterProceeding } } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow("") - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2304,19 +2206,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(expectedArgs) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2339,19 +2239,17 @@ final class GenericConstraintTests: XCTestCase, View { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow(expectedArgs) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2370,19 +2268,17 @@ final class GenericConstraintTests: XCTestCase, View { } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(ViewControllerWrapper.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(ViewControllerWrapper.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(ViewControllerWrapper.self)) } } @@ -2408,19 +2304,17 @@ final class ThenProceedOnAppTests: XCTestCase, App { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow("") - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2439,19 +2333,17 @@ final class ThenProceedOnAppTests: XCTestCase, App { } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(ViewControllerWrapper.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(ViewControllerWrapper.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(ViewControllerWrapper.self)) } } @@ -2477,19 +2369,17 @@ final class ThenProceedOnSceneTests: XCTestCase, Scene { let expectedArgs = UUID().uuidString let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).persistence(.removedAfterProceeding) - } - } + WorkflowView(launchingWith: expectedArgs) { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).persistence(.removedAfterProceeding) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(FR0.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(FR1.self).proceedInWorkflow("") - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(FR2.self)) XCTAssertEqual(try wfr2.find(FR2.self).actualView().persistence, .removedAfterProceeding) } @@ -2508,19 +2398,17 @@ final class ThenProceedOnSceneTests: XCTestCase, Scene { } let workflowView = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR0.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } - } + WorkflowView { + WorkflowItem(FR0.self) + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } - }.hostAndInspect(with: \.inspection).extractWorkflowItem() + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() try await workflowView.find(ViewControllerWrapper.self).proceedInWorkflow() - let wfr1 = try await workflowView.extractWrappedWorkflowItem() + let wfr1 = try await workflowView.extractWrappedWrapper() try await wfr1.find(ViewControllerWrapper.self).proceedInWorkflow() - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertNoThrow(try wfr2.find(ViewControllerWrapper.self)) } } diff --git a/Tests/SwiftCurrent_SwiftUITests/PersistenceTests.swift b/Tests/SwiftCurrent_SwiftUITests/PersistenceTests.swift index 9d9a2e763..d0d07c21e 100644 --- a/Tests/SwiftCurrent_SwiftUITests/PersistenceTests.swift +++ b/Tests/SwiftCurrent_SwiftUITests/PersistenceTests.swift @@ -34,15 +34,12 @@ final class PersistenceTests: XCTestCase, View { var body: some View { Text("FR4 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } - } - .persistence(.removedAfterProceeding) + WorkflowView { + WorkflowItem(FR1.self) + .persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } }.hostAndInspect(with: \.inspection) @@ -71,15 +68,12 @@ final class PersistenceTests: XCTestCase, View { var body: some View { Text("FR4 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) .persistence(.removedAfterProceeding) - } + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } }.hostAndInspect(with: \.inspection) @@ -112,14 +106,11 @@ final class PersistenceTests: XCTestCase, View { } let expectOnFinish = expectation(description: "OnFinish called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self).persistence(.removedAfterProceeding) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self).persistence(.removedAfterProceeding) } .onFinish { _ in expectOnFinish.fulfill() @@ -155,16 +146,13 @@ final class PersistenceTests: XCTestCase, View { var body: some View { Text("FR4 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - .persistence(.removedAfterProceeding) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + .persistence(.removedAfterProceeding) + WorkflowItem(FR3.self) .persistence(.removedAfterProceeding) - } + WorkflowItem(FR4.self) } }.hostAndInspect(with: \.inspection) @@ -198,17 +186,14 @@ final class PersistenceTests: XCTestCase, View { let binding = Binding(wrappedValue: true) let expectOnFinish = expectation(description: "OnFinish called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: binding) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self).persistence(.removedAfterProceeding) - } - .persistence(.removedAfterProceeding) - } + WorkflowView(isLaunched: binding) { + WorkflowItem(FR1.self) + .persistence(.removedAfterProceeding) + WorkflowItem(FR2.self) + .persistence(.removedAfterProceeding) + WorkflowItem(FR3.self) .persistence(.removedAfterProceeding) - } - .persistence(.removedAfterProceeding) + WorkflowItem(FR4.self).persistence(.removedAfterProceeding) } .onFinish { _ in expectOnFinish.fulfill() } }.hostAndInspect(with: \.inspection) @@ -251,20 +236,17 @@ final class PersistenceTests: XCTestCase, View { let expectOnFinish = expectation(description: "OnFinish called") let expectedStart = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: binding, startingArgs: expectedStart) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self).persistence(.removedAfterProceeding) - } - .persistence(.removedAfterProceeding) + WorkflowView(isLaunched: binding, launchingWith: expectedStart) { + WorkflowItem(FR1.self) + .persistence { + XCTAssertEqual($0, expectedStart) + return .removedAfterProceeding } + WorkflowItem(FR2.self) .persistence(.removedAfterProceeding) - } - .persistence { - XCTAssertEqual($0, expectedStart) - return .removedAfterProceeding - } + WorkflowItem(FR3.self) + .persistence(.removedAfterProceeding) + WorkflowItem(FR4.self).persistence(.removedAfterProceeding) } .onFinish { _ in expectOnFinish.fulfill() } }.hostAndInspect(with: \.inspection) @@ -305,22 +287,18 @@ final class PersistenceTests: XCTestCase, View { let expectOnFinish = expectation(description: "OnFinish called") let expectedStart = AnyWorkflow.PassedArgs.args(UUID().uuidString) let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: binding, startingArgs: expectedStart) { - thenProceed(with: FR1.self) { - - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self).persistence(.removedAfterProceeding) - } - .persistence(.removedAfterProceeding) + WorkflowView(isLaunched: binding, launchingWith: expectedStart) { + WorkflowItem(FR1.self) + .persistence { + XCTAssertNotNil(expectedStart.extractArgs(defaultValue: 1) as? String) + XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedStart.extractArgs(defaultValue: 1) as? String) + return .removedAfterProceeding } + WorkflowItem(FR2.self) .persistence(.removedAfterProceeding) - } - .persistence { - XCTAssertNotNil(expectedStart.extractArgs(defaultValue: 1) as? String) - XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedStart.extractArgs(defaultValue: 1) as? String) - return .removedAfterProceeding - } + WorkflowItem(FR3.self) + .persistence(.removedAfterProceeding) + WorkflowItem(FR4.self).persistence(.removedAfterProceeding) } .onFinish { _ in expectOnFinish.fulfill() } }.hostAndInspect(with: \.inspection) @@ -359,17 +337,14 @@ final class PersistenceTests: XCTestCase, View { let binding = Binding(wrappedValue: true) let expectOnFinish = expectation(description: "OnFinish called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: binding) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self).persistence(.removedAfterProceeding) - } - .persistence(.removedAfterProceeding) - } + WorkflowView(isLaunched: binding) { + WorkflowItem(FR1.self) + .persistence { .removedAfterProceeding } + WorkflowItem(FR2.self) + .persistence(.removedAfterProceeding) + WorkflowItem(FR3.self) .persistence(.removedAfterProceeding) - } - .persistence { .removedAfterProceeding } + WorkflowItem(FR4.self).persistence(.removedAfterProceeding) } .onFinish { _ in expectOnFinish.fulfill() } }.hostAndInspect(with: \.inspection) @@ -389,255 +364,255 @@ final class PersistenceTests: XCTestCase, View { } // MARK: PersistWhenSkippedTests -// func testPersistWhenSkipped_OnFirstItemInAWorkflow() throws { -// struct FR1: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR1 type") } -// func shouldLoad() -> Bool { false } -// } -// struct FR2: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR2 type") } -// } -// 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 expectViewLoaded = ViewHosting.loadView( -// WorkflowLauncher(isLaunched: .constant(true)) { -// thenProceed(with: FR1.self) { -// thenProceed(with: FR2.self) { -// thenProceed(with: FR3.self) { -// thenProceed(with: FR4.self) -// } -// } -// } -// .persistence(.persistWhenSkipped) -// } -// ).inspection.inspect { fr1 in -// try fr1.actualView().inspectWrapped { fr2 in -// XCTAssertNoThrow(try fr2.find(FR2.self).actualView().backUpInWorkflow()) -// try fr1.actualView().inspect { viewUnderTest in -// XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow()) -// try viewUnderTest.actualView().inspectWrapped { viewUnderTest in -// XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow()) -// try viewUnderTest.actualView().inspectWrapped { viewUnderTest in -// XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow()) -// try viewUnderTest.actualView().inspectWrapped { viewUnderTest in -// XCTAssertNoThrow(try viewUnderTest.find(FR4.self).actualView().proceedInWorkflow()) -// } -// } -// } -// } -// } -// } -// -// wait(for: [expectViewLoaded], timeout: TestConstant.timeout) -// } -// -// func testPersistWhenSkipped_OnMiddleItemInAWorkflow() throws { -// struct FR1: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR1 type") } -// } -// struct FR2: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR2 type") } -// func shouldLoad() -> Bool { false } -// } -// 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 expectViewLoaded = ViewHosting.loadView( -// WorkflowLauncher(isLaunched: .constant(true)) { -// thenProceed(with: FR1.self) { -// thenProceed(with: FR2.self) { -// thenProceed(with: FR3.self) { -// thenProceed(with: FR4.self) -// } -// } -// .persistence(.persistWhenSkipped) -// } -// } -// ).inspection.inspect { fr1 in -// XCTAssertNoThrow(try fr1.find(FR1.self).actualView().proceedInWorkflow()) -// try fr1.actualView().inspectWrapped { fr2 in -// XCTAssertThrowsError(try fr2.find(FR2.self)) -// try fr2.actualView().inspectWrapped { fr3 in -// XCTAssertNoThrow(try fr3.find(FR3.self).actualView().backUpInWorkflow()) -// try fr2.actualView().inspect { fr2 in -// XCTAssertNoThrow(try fr2.find(FR2.self).actualView().proceedInWorkflow()) -// try fr2.actualView().inspectWrapped { fr3 in -// XCTAssertNoThrow(try fr3.find(FR3.self).actualView().proceedInWorkflow()) -// try fr3.actualView().inspectWrapped { fr4 in -// XCTAssertNoThrow(try fr4.find(FR4.self).actualView().proceedInWorkflow()) -// } -// } -// } -// } -// } -// } -// -// wait(for: [expectViewLoaded], timeout: TestConstant.timeout) -// } -// -// func testPersistWhenSkipped_OnLastItemInAWorkflow() throws { -// struct FR1: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR1 type") } -// } -// struct FR2: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR2 type") } -// } -// 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") } -// func shouldLoad() -> Bool { false } -// } -// let expectOnFinish = expectation(description: "OnFinish called") -// let expectViewLoaded = ViewHosting.loadView( -// WorkflowLauncher(isLaunched: .constant(true)) { -// thenProceed(with: FR1.self) { -// thenProceed(with: FR2.self) { -// thenProceed(with: FR3.self) { -// thenProceed(with: FR4.self).persistence(.persistWhenSkipped) -// } -// } -// } -// } -// .onFinish { _ in expectOnFinish.fulfill() }) -// .inspection.inspect { viewUnderTest in -// XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow()) -// try viewUnderTest.actualView().inspectWrapped { viewUnderTest in -// XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow()) -// try viewUnderTest.actualView().inspectWrapped { viewUnderTest in -// XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow()) -// } -// } -// } -// -// wait(for: [expectViewLoaded, expectOnFinish], timeout: TestConstant.timeout) -// } -// -// func testPersistWhenSkipped_OnMultipleItemsInAWorkflow() throws { -// struct FR1: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR1 type") } -// } -// struct FR2: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR2 type") } -// func shouldLoad() -> Bool { false } -// } -// struct FR3: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR3 type") } -// func shouldLoad() -> Bool { false } -// } -// struct FR4: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR4 type") } -// } -// let expectViewLoaded = ViewHosting.loadView( -// WorkflowLauncher(isLaunched: .constant(true)) { -// thenProceed(with: FR1.self) { -// thenProceed(with: FR2.self) { -// thenProceed(with: FR3.self) { -// thenProceed(with: FR4.self) -// } -// .persistence(.persistWhenSkipped) -// } -// .persistence(.persistWhenSkipped) -// } -// } -// ).inspection.inspect { fr1 in -// XCTAssertNoThrow(try fr1.find(FR1.self).actualView().proceedInWorkflow()) -// try fr1.actualView().inspectWrapped { fr2 in -// XCTAssertThrowsError(try fr2.find(FR2.self)) -// try fr2.actualView().inspectWrapped { fr3 in -// XCTAssertThrowsError(try fr3.find(FR3.self)) -// try fr3.actualView().inspectWrapped { fr4 in -// XCTAssertNoThrow(try fr4.find(FR4.self).actualView().backUpInWorkflow()) -// try fr3.actualView().inspect { fr3 in -// XCTAssertNoThrow(try fr3.find(FR3.self).actualView().backUpInWorkflow()) -// try fr2.actualView().inspect { fr2 in -// XCTAssertNoThrow(try fr2.find(FR2.self).actualView().proceedInWorkflow()) -// try fr2.actualView().inspectWrapped { fr3 in -// XCTAssertThrowsError(try fr3.find(FR3.self)) -// try fr3.actualView().inspectWrapped { fr4 in -// XCTAssertNoThrow(try fr4.find(FR4.self).actualView().proceedInWorkflow()) -// } -// } -// } -// } -// } -// } -// } -// } -// wait(for: [expectViewLoaded], timeout: TestConstant.timeout) -// } -// -// func testPersistWhenSkipped_OnAllItemsInAWorkflow() throws { -// struct FR1: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR1 type") } -// func shouldLoad() -> Bool { false } -// } -// struct FR2: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR2 type") } -// func shouldLoad() -> Bool { false } -// } -// struct FR3: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR3 type") } -// func shouldLoad() -> Bool { false } -// } -// struct FR4: View, FlowRepresentable, Inspectable { -// var _workflowPointer: AnyFlowRepresentable? -// var body: some View { Text("FR4 type") } -// func shouldLoad() -> Bool { false } -// } -// let expectOnFinish = expectation(description: "OnFinish called") -// let expectViewLoaded = ViewHosting.loadView( -// WorkflowLauncher(isLaunched: .constant(true)) { -// thenProceed(with: FR1.self) { -// thenProceed(with: FR2.self) { -// thenProceed(with: FR3.self) { -// thenProceed(with: FR4.self).persistence(.persistWhenSkipped) -// } -// .persistence(.persistWhenSkipped) -// } -// .persistence(.persistWhenSkipped) -// } -// .persistence(.persistWhenSkipped) -// } -// .onFinish { _ in expectOnFinish.fulfill() }) -// .inspection.inspect { fr1 in -// try fr1.actualView().inspectWrapped { fr2 in -// XCTAssertThrowsError(try fr2.find(FR2.self)) -// try fr2.actualView().inspectWrapped { fr3 in -// XCTAssertThrowsError(try fr3.find(FR3.self)) -// try fr3.actualView().inspectWrapped { fr4 in -// XCTAssertNoThrow(try fr4.find(FR4.self)) -// } -// } -// } -// } -// wait(for: [expectOnFinish, expectViewLoaded], timeout: TestConstant.timeout) -// } + // func testPersistWhenSkipped_OnFirstItemInAWorkflow() throws { + // struct FR1: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR1 type") } + // func shouldLoad() -> Bool { false } + // } + // struct FR2: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR2 type") } + // } + // 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 expectViewLoaded = ViewHosting.loadView( + // WorkflowLauncher(isLaunched: .constant(true)) { + // thenProceed(with: FR1.self) { + // thenProceed(with: FR2.self) { + // thenProceed(with: FR3.self) { + // thenProceed(with: FR4.self) + // } + // } + // } + // .persistence(.persistWhenSkipped) + // } + // ).inspection.inspect { fr1 in + // try fr1.actualView().inspectWrapped { fr2 in + // XCTAssertNoThrow(try fr2.find(FR2.self).actualView().backUpInWorkflow()) + // try fr1.actualView().inspect { viewUnderTest in + // XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow()) + // try viewUnderTest.actualView().inspectWrapped { viewUnderTest in + // XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow()) + // try viewUnderTest.actualView().inspectWrapped { viewUnderTest in + // XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow()) + // try viewUnderTest.actualView().inspectWrapped { viewUnderTest in + // XCTAssertNoThrow(try viewUnderTest.find(FR4.self).actualView().proceedInWorkflow()) + // } + // } + // } + // } + // } + // } + // + // wait(for: [expectViewLoaded], timeout: TestConstant.timeout) + // } + // + // func testPersistWhenSkipped_OnMiddleItemInAWorkflow() throws { + // struct FR1: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR1 type") } + // } + // struct FR2: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR2 type") } + // func shouldLoad() -> Bool { false } + // } + // 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 expectViewLoaded = ViewHosting.loadView( + // WorkflowLauncher(isLaunched: .constant(true)) { + // thenProceed(with: FR1.self) { + // thenProceed(with: FR2.self) { + // thenProceed(with: FR3.self) { + // thenProceed(with: FR4.self) + // } + // } + // .persistence(.persistWhenSkipped) + // } + // } + // ).inspection.inspect { fr1 in + // XCTAssertNoThrow(try fr1.find(FR1.self).actualView().proceedInWorkflow()) + // try fr1.actualView().inspectWrapped { fr2 in + // XCTAssertThrowsError(try fr2.find(FR2.self)) + // try fr2.actualView().inspectWrapped { fr3 in + // XCTAssertNoThrow(try fr3.find(FR3.self).actualView().backUpInWorkflow()) + // try fr2.actualView().inspect { fr2 in + // XCTAssertNoThrow(try fr2.find(FR2.self).actualView().proceedInWorkflow()) + // try fr2.actualView().inspectWrapped { fr3 in + // XCTAssertNoThrow(try fr3.find(FR3.self).actualView().proceedInWorkflow()) + // try fr3.actualView().inspectWrapped { fr4 in + // XCTAssertNoThrow(try fr4.find(FR4.self).actualView().proceedInWorkflow()) + // } + // } + // } + // } + // } + // } + // + // wait(for: [expectViewLoaded], timeout: TestConstant.timeout) + // } + // + // func testPersistWhenSkipped_OnLastItemInAWorkflow() throws { + // struct FR1: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR1 type") } + // } + // struct FR2: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR2 type") } + // } + // 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") } + // func shouldLoad() -> Bool { false } + // } + // let expectOnFinish = expectation(description: "OnFinish called") + // let expectViewLoaded = ViewHosting.loadView( + // WorkflowLauncher(isLaunched: .constant(true)) { + // thenProceed(with: FR1.self) { + // thenProceed(with: FR2.self) { + // thenProceed(with: FR3.self) { + // thenProceed(with: FR4.self).persistence(.persistWhenSkipped) + // } + // } + // } + // } + // .onFinish { _ in expectOnFinish.fulfill() }) + // .inspection.inspect { viewUnderTest in + // XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow()) + // try viewUnderTest.actualView().inspectWrapped { viewUnderTest in + // XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow()) + // try viewUnderTest.actualView().inspectWrapped { viewUnderTest in + // XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow()) + // } + // } + // } + // + // wait(for: [expectViewLoaded, expectOnFinish], timeout: TestConstant.timeout) + // } + // + // func testPersistWhenSkipped_OnMultipleItemsInAWorkflow() throws { + // struct FR1: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR1 type") } + // } + // struct FR2: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR2 type") } + // func shouldLoad() -> Bool { false } + // } + // struct FR3: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR3 type") } + // func shouldLoad() -> Bool { false } + // } + // struct FR4: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR4 type") } + // } + // let expectViewLoaded = ViewHosting.loadView( + // WorkflowLauncher(isLaunched: .constant(true)) { + // thenProceed(with: FR1.self) { + // thenProceed(with: FR2.self) { + // thenProceed(with: FR3.self) { + // thenProceed(with: FR4.self) + // } + // .persistence(.persistWhenSkipped) + // } + // .persistence(.persistWhenSkipped) + // } + // } + // ).inspection.inspect { fr1 in + // XCTAssertNoThrow(try fr1.find(FR1.self).actualView().proceedInWorkflow()) + // try fr1.actualView().inspectWrapped { fr2 in + // XCTAssertThrowsError(try fr2.find(FR2.self)) + // try fr2.actualView().inspectWrapped { fr3 in + // XCTAssertThrowsError(try fr3.find(FR3.self)) + // try fr3.actualView().inspectWrapped { fr4 in + // XCTAssertNoThrow(try fr4.find(FR4.self).actualView().backUpInWorkflow()) + // try fr3.actualView().inspect { fr3 in + // XCTAssertNoThrow(try fr3.find(FR3.self).actualView().backUpInWorkflow()) + // try fr2.actualView().inspect { fr2 in + // XCTAssertNoThrow(try fr2.find(FR2.self).actualView().proceedInWorkflow()) + // try fr2.actualView().inspectWrapped { fr3 in + // XCTAssertThrowsError(try fr3.find(FR3.self)) + // try fr3.actualView().inspectWrapped { fr4 in + // XCTAssertNoThrow(try fr4.find(FR4.self).actualView().proceedInWorkflow()) + // } + // } + // } + // } + // } + // } + // } + // } + // wait(for: [expectViewLoaded], timeout: TestConstant.timeout) + // } + // + // func testPersistWhenSkipped_OnAllItemsInAWorkflow() throws { + // struct FR1: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR1 type") } + // func shouldLoad() -> Bool { false } + // } + // struct FR2: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR2 type") } + // func shouldLoad() -> Bool { false } + // } + // struct FR3: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR3 type") } + // func shouldLoad() -> Bool { false } + // } + // struct FR4: View, FlowRepresentable, Inspectable { + // var _workflowPointer: AnyFlowRepresentable? + // var body: some View { Text("FR4 type") } + // func shouldLoad() -> Bool { false } + // } + // let expectOnFinish = expectation(description: "OnFinish called") + // let expectViewLoaded = ViewHosting.loadView( + // WorkflowLauncher(isLaunched: .constant(true)) { + // thenProceed(with: FR1.self) { + // thenProceed(with: FR2.self) { + // thenProceed(with: FR3.self) { + // thenProceed(with: FR4.self).persistence(.persistWhenSkipped) + // } + // .persistence(.persistWhenSkipped) + // } + // .persistence(.persistWhenSkipped) + // } + // .persistence(.persistWhenSkipped) + // } + // .onFinish { _ in expectOnFinish.fulfill() }) + // .inspection.inspect { fr1 in + // try fr1.actualView().inspectWrapped { fr2 in + // XCTAssertThrowsError(try fr2.find(FR2.self)) + // try fr2.actualView().inspectWrapped { fr3 in + // XCTAssertThrowsError(try fr3.find(FR3.self)) + // try fr3.actualView().inspectWrapped { fr4 in + // XCTAssertNoThrow(try fr4.find(FR4.self)) + // } + // } + // } + // } + // wait(for: [expectOnFinish, expectViewLoaded], timeout: TestConstant.timeout) + // } } diff --git a/Tests/SwiftCurrent_SwiftUITests/SkipTests.swift b/Tests/SwiftCurrent_SwiftUITests/SkipTests.swift index d20bd15e4..b481526ff 100644 --- a/Tests/SwiftCurrent_SwiftUITests/SkipTests.swift +++ b/Tests/SwiftCurrent_SwiftUITests/SkipTests.swift @@ -35,14 +35,11 @@ final class SkipTests: XCTestCase, View { var body: some View { Text("FR4 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } }.hostAndInspect(with: \.inspection) @@ -71,14 +68,11 @@ final class SkipTests: XCTestCase, View { var body: some View { Text("FR4 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } }.hostAndInspect(with: \.inspection) @@ -108,14 +102,11 @@ final class SkipTests: XCTestCase, View { } let expectOnFinish = expectation(description: "OnFinish called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } .onFinish { _ in expectOnFinish.fulfill() } }.hostAndInspect(with: \.inspection) @@ -149,14 +140,11 @@ final class SkipTests: XCTestCase, View { var body: some View { Text("FR4 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } }.hostAndInspect(with: \.inspection) @@ -189,14 +177,11 @@ final class SkipTests: XCTestCase, View { } let expectOnFinish = expectation(description: "OnFinish called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } .onFinish { _ in expectOnFinish.fulfill() } }.hostAndInspect(with: \.inspection) diff --git a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_ModalTests.swift b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_ModalTests.swift index 67d2e0c42..bf1d918f8 100644 --- a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_ModalTests.swift +++ b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_ModalTests.swift @@ -23,6 +23,17 @@ extension InspectableView where View == ViewType.Sheet { @available(iOS 15.0, macOS 11, tvOS 14.0, watchOS 7.0, *) final class SwiftCurrent_ModalTests: XCTestCase, Scene { + func testModalModifier() throws { + let sampleView = Text("Test") + let binding = Binding(wrappedValue: true) + let viewUnderTest = try sampleView.modal(isPresented: binding, style: .sheet, destination: Text("nextView")).inspect() + XCTAssertNoThrow(try viewUnderTest.sheet()) + XCTAssert(try viewUnderTest.sheet().isPresented()) + XCTAssertEqual(try viewUnderTest.sheet().text().string(), "nextView") + binding.wrappedValue = false + XCTAssertThrowsError(try viewUnderTest.sheet()) + } + func testWorkflowCanBeFollowed() async throws { struct FR1: View, FlowRepresentable, Inspectable { var _workflowPointer: AnyFlowRepresentable? @@ -34,9 +45,45 @@ final class SwiftCurrent_ModalTests: XCTestCase, Scene { } let expectOnFinish = expectation(description: "OnFinish called") let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self).presentationType(.modal) + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).presentationType(.modal) + } + .onFinish { _ in + expectOnFinish.fulfill() + } + } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try wfr1.findModalModifier()) + try await wfr1.find(FR1.self).proceedInWorkflow() + let wfr2 = try await wfr1.extractWrappedWrapper() + + let fr2 = try wfr2.find(FR2.self) + XCTAssertEqual(try fr2.text().string(), "FR2 type") + try await fr2.proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBeFollowed_WithWorkflowGroup() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowGroup { + WorkflowItem(FR2.self).presentationType(.modal) } } .onFinish { _ in @@ -44,64 +91,170 @@ final class SwiftCurrent_ModalTests: XCTestCase, Scene { } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try wfr1.findModalModifier()) + try await wfr1.find(FR1.self).proceedInWorkflow() + let wfr2 = try await wfr1.extractWrappedWrapper() + + let fr2 = try wfr2.find(FR2.self) + XCTAssertEqual(try fr2.text().string(), "FR2 type") + try await fr2.proceedInWorkflow() - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBeFollowed_WithOptionalWorkflowItem_WhenTrue() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if true { + WorkflowItem(FR2.self).presentationType(.modal) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try wfr1.findModalModifier()) try await wfr1.find(FR1.self).proceedInWorkflow() - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr1.find(ViewType.Sheet.self).isPresented()) + let wfr2 = try await wfr1.extractWrappedWrapper() - let fr2 = try wfr1.find(ViewType.Sheet.self).find(FR2.self) + let fr2 = try wfr2.find(FR2.self) XCTAssertEqual(try fr2.text().string(), "FR2 type") try await fr2.proceedInWorkflow() wait(for: [expectOnFinish], timeout: TestConstant.timeout) } - func testWorkflowItemsOfTheSameTypeCanBeFollowed() async throws { + func testWorkflowCanBeFollowed_WithEitherWorkflowItem_WhenTrue() async throws { struct FR1: View, FlowRepresentable, Inspectable { var _workflowPointer: AnyFlowRepresentable? var body: some View { Text("FR1 type") } } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if true { + WorkflowItem(FR2.self).presentationType(.modal) + } else { + WorkflowItem(FR3.self).presentationType(.modal) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } + } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try wfr1.findModalModifier()) + try await wfr1.find(FR1.self).proceedInWorkflow() + let wfr2 = try await wfr1.extractWrappedWrapper() + + let fr2 = try wfr2.find(FR2.self) + XCTAssertEqual(try fr2.text().string(), "FR2 type") + try await fr2.proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + func testWorkflowCanBeFollowed_WithEitherWorkflowItem_WhenFalse() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR1.self).presentationType(.modal) - }.presentationType(.modal) + WorkflowView { + WorkflowItem(FR1.self) + if false { + WorkflowItem(FR2.self).presentationType(.modal) + } else { + WorkflowItem(FR3.self).presentationType(.modal) } } + .onFinish { _ in + expectOnFinish.fulfill() + } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try wfr1.findModalModifier()) + try await wfr1.find(FR1.self).proceedInWorkflow() + let wfr2 = try await wfr1.extractWrappedWrapper() - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) + let fr3 = try wfr2.find(FR3.self) + XCTAssertEqual(try fr3.text().string(), "FR3 type") + try await fr3.proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowItemsOfTheSameTypeCanBeFollowed() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) + + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR1.self).presentationType(.modal) + WorkflowItem(FR1.self).presentationType(.modal) + } } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + XCTAssertNoThrow(try wfr1.findModalModifier()) try await wfr1.find(FR1.self).proceedInWorkflow() - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr1.find(ViewType.Sheet.self).isPresented()) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertNoThrow(try wfr2.findModalModifier()) try await wfr2.find(FR1.self).proceedInWorkflow() - try await wfr2.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr2.find(ViewType.Sheet.self).isPresented()) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() try await wfr3.find(FR1.self).proceedInWorkflow() - try await wfr3.actualView().host { $0.environmentObject(model).environmentObject(launcher) } } func testLargeWorkflowCanBeFollowed() async throws { @@ -135,62 +288,44 @@ final class SwiftCurrent_ModalTests: XCTestCase, Scene { } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) { - thenProceed(with: FR5.self) { - thenProceed(with: FR6.self) { - thenProceed(with: FR7.self).presentationType(.modal) - }.presentationType(.modal) - }.presentationType(.modal) - }.presentationType(.modal) - }.presentationType(.modal) - }.presentationType(.modal) - } + WorkflowView { + WorkflowItem(FR1.self).presentationType(.modal) + WorkflowItem(FR2.self).presentationType(.modal) + WorkflowItem(FR3.self).presentationType(.modal) + WorkflowItem(FR4.self).presentationType(.modal) + WorkflowItem(FR5.self).presentationType(.modal) + WorkflowItem(FR6.self).presentationType(.modal) + WorkflowItem(FR7.self).presentationType(.modal) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() - - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + XCTAssertNoThrow(try wfr1.findModalModifier()) try await wfr1.find(FR1.self).proceedInWorkflow() - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr1.find(ViewType.Sheet.self).isPresented()) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertNoThrow(try wfr2.findModalModifier()) try await wfr2.find(FR2.self).proceedInWorkflow() - try await wfr2.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr2.find(ViewType.Sheet.self).isPresented()) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() + XCTAssertNoThrow(try wfr3.findModalModifier()) try await wfr3.find(FR3.self).proceedInWorkflow() - try await wfr3.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr3.find(ViewType.Sheet.self).isPresented()) - let wfr4 = try await wfr3.extractWrappedWorkflowItem() + let wfr4 = try await wfr3.extractWrappedWrapper() + XCTAssertNoThrow(try wfr4.findModalModifier()) try await wfr4.find(FR4.self).proceedInWorkflow() - try await wfr4.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr4.find(ViewType.Sheet.self).isPresented()) - let wfr5 = try await wfr4.extractWrappedWorkflowItem() + let wfr5 = try await wfr4.extractWrappedWrapper() + XCTAssertNoThrow(try wfr5.findModalModifier()) try await wfr5.find(FR5.self).proceedInWorkflow() - try await wfr5.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr5.find(ViewType.Sheet.self).isPresented()) - let wfr6 = try await wfr5.extractWrappedWorkflowItem() + let wfr6 = try await wfr5.extractWrappedWrapper() + XCTAssertNoThrow(try wfr6.findModalModifier()) try await wfr6.find(FR6.self).proceedInWorkflow() - try await wfr6.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr6.find(ViewType.Sheet.self).isPresented()) - let wfr7 = try await wfr6.extractWrappedWorkflowItem() + let wfr7 = try await wfr6.extractWrappedWrapper() try await wfr7.find(FR7.self).proceedInWorkflow() } @@ -209,33 +344,23 @@ final class SwiftCurrent_ModalTests: XCTestCase, Scene { var body: some View { Text("FR3 type") } } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self).presentationType(.modal) - }.presentationType(.modal) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).presentationType(.modal) + WorkflowItem(FR3.self).presentationType(.modal) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() - - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() XCTAssertThrowsError(try wfr1.find(FR1.self)) - XCTAssertNoThrow(try wfr1.find(FR2.self)) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertNoThrow(try wfr2.findModalModifier()) try await wfr2.find(FR2.self).proceedInWorkflow() - try await wfr2.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr2.find(ViewType.Sheet.self).isPresented()) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() try await wfr3.find(FR3.self).proceedInWorkflow() } @@ -255,33 +380,23 @@ final class SwiftCurrent_ModalTests: XCTestCase, Scene { } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self).presentationType(.modal) - }.presentationType(.modal) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).presentationType(.modal) + WorkflowItem(FR3.self).presentationType(.modal) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() - - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + XCTAssertNoThrow(try wfr1.findModalModifier()) try await wfr1.find(FR1.self).proceedInWorkflow() - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr1.find(ViewType.Sheet.self).isPresented()) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertThrowsError(try wfr2.find(FR2.self)) - XCTAssertNoThrow(try wfr2.find(FR3.self)) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() try await wfr3.find(FR3.self).proceedInWorkflow() } @@ -306,39 +421,27 @@ final class SwiftCurrent_ModalTests: XCTestCase, Scene { } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self).presentationType(.modal) - } - }.presentationType(.modal) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).presentationType(.modal) + WorkflowItem(FR3.self).presentationType(.modal) + WorkflowItem(FR4.self).presentationType(.modal) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() - - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + XCTAssertNoThrow(try wfr1.findModalModifier()) try await wfr1.find(FR1.self).proceedInWorkflow() - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr1.find(ViewType.Sheet.self).isPresented()) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertThrowsError(try wfr2.find(FR2.self)) - XCTAssertNoThrow(try wfr2.find(FR4.self)) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() XCTAssertThrowsError(try wfr3.find(FR3.self)) - XCTAssertNoThrow(try wfr3.find(FR4.self)) - let wfr4 = try await wfr3.extractWrappedWorkflowItem() + let wfr4 = try await wfr3.extractWrappedWrapper() try await wfr4.find(FR4.self).proceedInWorkflow() } @@ -359,36 +462,28 @@ final class SwiftCurrent_ModalTests: XCTestCase, Scene { let expectOnFinish = expectation(description: "onFinish called") let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self).presentationType(.modal) - }.presentationType(.modal) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self).presentationType(.modal) + WorkflowItem(FR3.self).presentationType(.modal) } .onFinish { _ in expectOnFinish.fulfill() } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() - - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + XCTAssertNoThrow(try wfr1.findModalModifier()) try await wfr1.find(FR1.self).proceedInWorkflow() - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertTrue(try wfr1.find(ViewType.Sheet.self).isPresented()) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertNoThrow(try wfr2.findModalModifier()) try await wfr2.find(FR2.self).proceedInWorkflow() XCTAssertThrowsError(try wfr2.find(FR3.self)) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() XCTAssertThrowsError(try wfr3.find(FR3.self)) wait(for: [expectOnFinish], timeout: TestConstant.timeout) diff --git a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_NavigationLinkTests.swift b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_NavigationLinkTests.swift index ea792edfc..bbfaeaa4d 100644 --- a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_NavigationLinkTests.swift +++ b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_NavigationLinkTests.swift @@ -26,35 +26,234 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { } let expectOnFinish = expectation(description: "OnFinish called") let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR2.self) } .onFinish { _ in expectOnFinish.fulfill() } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) + print(type(of: wfr1)) + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + + try await wfr1.proceedAndCheckNavLink(on: FR1.self) + + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertEqual(try wfr2.find(FR2.self).text().string(), "FR2 type") + try await wfr2.find(FR2.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBeFollowed_WithWorkflowGroup() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowGroup { + WorkflowItem(FR2.self) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + print(type(of: wfr1)) XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") - XCTAssertFalse(try wfr1.find(ViewType.NavigationLink.self).isActive()) - try await wfr1.find(FR1.self).proceedInWorkflow() - // needed to re-host to avoid some kind of race with the nav link - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } + try await wfr1.proceedAndCheckNavLink(on: FR1.self) + + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertEqual(try wfr2.find(FR2.self).text().string(), "FR2 type") + try await wfr2.find(FR2.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBeFollowed_WithBuildOptions_WhenTrue() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + if true { + WorkflowItem(FR2.self) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } + } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + print(type(of: wfr1)) + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + + try await wfr1.proceedAndCheckNavLink(on: FR1.self) + + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertEqual(try wfr2.find(FR2.self).text().string(), "FR2 type") + try await wfr2.find(FR2.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBeFollowed_WithBuildOptions_WhenFalse() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + if false { + WorkflowItem(FR2.self) + } + WorkflowItem(FR3.self) + } + .onFinish { _ in + expectOnFinish.fulfill() + } + } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + print(type(of: wfr1)) + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + + try await wfr1.proceedAndCheckNavLink(on: FR1.self) + + let wfr3 = try await wfr1.extractWrappedWrapper().extractWrappedWrapper() + XCTAssertEqual(try wfr3.find(FR3.self).text().string(), "FR3 type") + try await wfr3.find(FR3.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBeFollowed_WithBuildEither_WhenTrue() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + if true { + WorkflowItem(FR2.self) + } else { + WorkflowItem(FR3.self) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } + } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + print(type(of: wfr1)) + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + + try await wfr1.proceedAndCheckNavLink(on: FR1.self) + + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertEqual(try wfr2.find(FR2.self).text().string(), "FR2 type") + try await wfr2.find(FR2.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBeFollowed_WithBuildEither_WhenFalse() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let wfr1 = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + if false { + WorkflowItem(FR2.self) + } else { + WorkflowItem(FR3.self) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } + } + .hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() + + print(type(of: wfr1)) + + XCTAssertEqual(try wfr1.find(FR1.self).text().string(), "FR1 type") + + try await wfr1.proceedAndCheckNavLink(on: FR1.self) - XCTAssertTrue(try wfr1.find(ViewType.NavigationLink.self).isActive()) - XCTAssertEqual(try wfr1.find(FR2.self).text().string(), "FR2 type") - try await wfr1.find(FR2.self).proceedInWorkflow() + let wfr2 = try await wfr1.extractWrappedWrapper() + XCTAssertEqual(try wfr2.find(FR3.self).text().string(), "FR3 type") + try await wfr2.find(FR3.self).proceedInWorkflow() wait(for: [expectOnFinish], timeout: TestConstant.timeout) } @@ -66,16 +265,15 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR1.self) { - thenProceed(with: FR1.self) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR1.self) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() let model = try await MainActor.run { try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) @@ -90,13 +288,13 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } XCTAssert(try wfr1.find(ViewType.NavigationLink.self).isActive()) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertFalse(try wfr2.find(ViewType.NavigationLink.self).isActive()) try await wfr2.find(FR1.self).proceedInWorkflow() try await wfr2.actualView().host { $0.environmentObject(model).environmentObject(launcher) } XCTAssert(try wfr2.find(ViewType.NavigationLink.self).isActive()) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() try await wfr3.find(FR1.self).proceedInWorkflow() } @@ -131,69 +329,38 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) { - thenProceed(with: FR5.self) { - thenProceed(with: FR6.self) { - thenProceed(with: FR7.self) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR2.self).presentationType(.navigationLink) + WorkflowItem(FR3.self).presentationType(.navigationLink) + WorkflowItem(FR4.self).presentationType(.navigationLink) + WorkflowItem(FR5.self).presentationType(.navigationLink) + WorkflowItem(FR6.self).presentationType(.navigationLink) + WorkflowItem(FR7.self).presentationType(.navigationLink) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + try await wfr1.proceedAndCheckNavLink(on: FR1.self) - XCTAssertFalse(try wfr1.find(ViewType.NavigationLink.self).isActive()) - try await wfr1.find(FR1.self).proceedInWorkflow() - // needed to re-host to avoid some kind of race with the nav link - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr1.find(ViewType.NavigationLink.self).isActive()) + let wfr2 = try await wfr1.extractWrappedWrapper() + try await wfr2.proceedAndCheckNavLink(on: FR2.self) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() - XCTAssertFalse(try wfr2.find(ViewType.NavigationLink.self).isActive()) - try await wfr2.find(FR2.self).proceedInWorkflow() - try await wfr2.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr2.find(ViewType.NavigationLink.self).isActive()) + let wfr3 = try await wfr2.extractWrappedWrapper() + try await wfr3.proceedAndCheckNavLink(on: FR3.self) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() - XCTAssertFalse(try wfr3.find(ViewType.NavigationLink.self).isActive()) - try await wfr3.find(FR3.self).proceedInWorkflow() - try await wfr3.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr3.find(ViewType.NavigationLink.self).isActive()) - - let wfr4 = try await wfr3.extractWrappedWorkflowItem() - XCTAssertFalse(try wfr4.find(ViewType.NavigationLink.self).isActive()) - try await wfr4.find(FR4.self).proceedInWorkflow() - try await wfr4.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr4.find(ViewType.NavigationLink.self).isActive()) - - let wfr5 = try await wfr4.extractWrappedWorkflowItem() - XCTAssertFalse(try wfr5.find(ViewType.NavigationLink.self).isActive()) - try await wfr5.find(FR5.self).proceedInWorkflow() - try await wfr5.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr5.find(ViewType.NavigationLink.self).isActive()) - - let wfr6 = try await wfr5.extractWrappedWorkflowItem() - XCTAssertFalse(try wfr6.find(ViewType.NavigationLink.self).isActive()) - try await wfr6.find(FR6.self).proceedInWorkflow() - try await wfr6.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr6.find(ViewType.NavigationLink.self).isActive()) - - let wfr7 = try await wfr6.extractWrappedWorkflowItem() + let wfr4 = try await wfr3.extractWrappedWrapper() + try await wfr4.proceedAndCheckNavLink(on: FR4.self) + + let wfr5 = try await wfr4.extractWrappedWrapper() + try await wfr5.proceedAndCheckNavLink(on: FR5.self) + + let wfr6 = try await wfr5.extractWrappedWrapper() + try await wfr6.proceedAndCheckNavLink(on: FR6.self) + + let wfr7 = try await wfr6.extractWrappedWrapper() try await wfr7.find(FR7.self).proceedInWorkflow() } @@ -212,33 +379,22 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { var body: some View { Text("FR3 type") } } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR2.self).presentationType(.navigationLink) + WorkflowItem(FR3.self) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() - - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() XCTAssertThrowsError(try wfr1.find(FR1.self).actualView()) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() - XCTAssertFalse(try wfr2.find(ViewType.NavigationLink.self).isActive()) - try await wfr2.find(FR2.self).proceedInWorkflow() - try await wfr2.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr2.find(ViewType.NavigationLink.self).isActive()) + let wfr2 = try await wfr1.extractWrappedWrapper() + try await wfr2.proceedAndCheckNavLink(on: FR2.self) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() XCTAssertNoThrow(try wfr3.find(FR3.self).actualView()) } @@ -257,33 +413,22 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { var body: some View { Text("FR3 type") } } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR2.self).presentationType(.navigationLink) + WorkflowItem(FR3.self) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } - XCTAssertFalse(try wfr1.find(ViewType.NavigationLink.self).isActive()) - try await wfr1.find(FR1.self).proceedInWorkflow() - // needed to re-host to avoid some kind of race with the nav link - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr1.find(ViewType.NavigationLink.self).isActive()) + try await wfr1.proceedAndCheckNavLink(on: FR1.self) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertThrowsError(try wfr2.find(FR2.self)) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() XCTAssertNoThrow(try wfr3.find(FR3.self).actualView()) } @@ -307,38 +452,26 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { var body: some View { Text("FR3 type") } } let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR2.self).presentationType(.navigationLink) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } - XCTAssertFalse(try wfr1.find(ViewType.NavigationLink.self).isActive()) - try await wfr1.find(FR1.self).proceedInWorkflow() - // needed to re-host to avoid some kind of race with the nav link - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr1.find(ViewType.NavigationLink.self).isActive()) + try await wfr1.proceedAndCheckNavLink(on: FR1.self) - let wfr2 = try await wfr1.extractWrappedWorkflowItem() + let wfr2 = try await wfr1.extractWrappedWrapper() XCTAssertThrowsError(try wfr2.find(FR2.self)) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() XCTAssertThrowsError(try wfr3.find(FR3.self).actualView()) - let wfr4 = try await wfr3.extractWrappedWorkflowItem() + let wfr4 = try await wfr3.extractWrappedWrapper() XCTAssertNoThrow(try wfr4.find(FR4.self).actualView()) } @@ -359,62 +492,55 @@ final class SwiftCurrent_NavigationLinkTests: XCTestCase, View { let expectOnFinish = expectation(description: "onFinish called") let wfr1 = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) - }.presentationType(.navigationLink) - }.presentationType(.navigationLink) + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + WorkflowItem(FR2.self).presentationType(.navigationLink) + WorkflowItem(FR3.self) } .onFinish { _ in expectOnFinish.fulfill() } } .hostAndInspect(with: \.inspection) - .extractWorkflowItem() + .extractWorkflowLauncher() + .extractWorkflowItemWrapper() - let model = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_model") as? EnvironmentObject)?.wrappedValue) - } - let launcher = try await MainActor.run { - try XCTUnwrap((Mirror(reflecting: try wfr1.actualView()).descendant("_launcher") as? EnvironmentObject)?.wrappedValue) - } + try await wfr1.proceedAndCheckNavLink(on: FR1.self) - XCTAssertFalse(try wfr1.find(ViewType.NavigationLink.self).isActive()) - try await wfr1.find(FR1.self).proceedInWorkflow() - // needed to re-host to avoid some kind of race with the nav link - try await wfr1.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssert(try wfr1.find(ViewType.NavigationLink.self).isActive()) - - let wfr2 = try await wfr1.extractWrappedWorkflowItem() - XCTAssertFalse(try wfr2.find(ViewType.NavigationLink.self).isActive()) - try await wfr2.find(FR2.self).proceedInWorkflow() - try await wfr2.actualView().host { $0.environmentObject(model).environmentObject(launcher) } - XCTAssertFalse(try wfr2.find(ViewType.NavigationLink.self).isActive()) + let wfr2 = try await wfr1.extractWrappedWrapper() + try await wfr2.proceedAndCheckNavLink(on: FR2.self) - let wfr3 = try await wfr2.extractWrappedWorkflowItem() + let wfr3 = try await wfr2.extractWrappedWrapper() XCTAssertThrowsError(try wfr3.find(FR3.self)) XCTAssertNoThrow(try wfr2.find(FR2.self)) wait(for: [expectOnFinish], timeout: TestConstant.timeout) } - func testConvenienceEmbedInNavViewFunction() throws { + func testConvenienceEmbedInNavViewFunction() async throws { struct FR1: View, FlowRepresentable, Inspectable { var _workflowPointer: AnyFlowRepresentable? var body: some View { Text("FR1 type") } } - let launcherView = WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self).presentationType(.navigationLink) - }.embedInNavigationView() + let launcherView = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self).presentationType(.navigationLink) + }.embedInNavigationView() + }.hostAndInspect(with: \.inspection) + .extractWorkflowLauncher() - let expectViewLoaded = launcherView.inspection.inspect { launcher in - let navView = try launcher.navigationView() - XCTAssert(try navView.navigationViewStyle() is StackNavigationViewStyle) - XCTAssertNoThrow(try navView.view(WorkflowItem.self, 0)) - } - ViewHosting.host(view: launcherView) - wait(for: [expectViewLoaded], timeout: TestConstant.timeout) + let navView = try launcherView.navigationView() + XCTAssert(try navView.navigationViewStyle() is StackNavigationViewStyle) + XCTAssertNoThrow(try navView.view(WorkflowItemWrapper, Never>.self, 0)) + } +} + +@available(iOS 15.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension InspectableView where View: CustomViewType & SingleViewContent, View.T: _WorkflowItemProtocol { + fileprivate func proceedAndCheckNavLink(on: FR.Type) async throws where FR.WorkflowOutput == Never { + XCTAssertFalse(try find(ViewType.NavigationLink.self).isActive()) + + try await find(FR.self).proceedInWorkflow() } } diff --git a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift index 9e2bdd921..38aeb09ec 100644 --- a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift +++ b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUITests.swift @@ -26,9 +26,104 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } let expectOnFinish = expectation(description: "OnFinish called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection) + + XCTAssertEqual(try launcher.find(FR1.self).text().string(), "FR1 type") + try await launcher.find(FR1.self).proceedInWorkflow() + let fr2 = try launcher.find(FR2.self) + XCTAssertEqual(try fr2.text().string(), "FR2 type") + XCTAssertNoThrow(try fr2.actualView().proceedInWorkflow()) + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBuildOptionalItem_WhenTrue() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let launcher = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if true { + WorkflowItem(FR2.self) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection) + + XCTAssertEqual(try launcher.find(FR1.self).text().string(), "FR1 type") + try await launcher.find(FR1.self).proceedInWorkflow() + let fr2 = try launcher.find(FR2.self) + XCTAssertEqual(try fr2.text().string(), "FR2 type") + XCTAssertNoThrow(try fr2.actualView().proceedInWorkflow()) + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBuildOptionalItem_WhenFalse() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let launcher = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if false { + WorkflowItem(FR2.self) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection) + + XCTAssertEqual(try launcher.find(FR1.self).text().string(), "FR1 type") + try await launcher.find(FR1.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanBuildEitherItem_WhenTrue() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let launcher = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if true { + WorkflowItem(FR2.self) + } else { + WorkflowItem(FR3.self) } } .onFinish { _ in @@ -45,6 +140,43 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { wait(for: [expectOnFinish], timeout: TestConstant.timeout) } + func testWorkflowCanBuildEitherItem_WhenFalse() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let launcher = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if false { + WorkflowItem(FR2.self) + } else { + WorkflowItem(FR3.self) + } + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection) + + XCTAssertEqual(try launcher.find(FR1.self).text().string(), "FR1 type") + try await launcher.find(FR1.self).proceedInWorkflow() + let fr2 = try launcher.find(FR3.self) + XCTAssertEqual(try fr2.text().string(), "FR3 type") + XCTAssertNoThrow(try fr2.actualView().proceedInWorkflow()) + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + func testWorkflowCanHaveMultipleOnFinishClosures() async throws { struct FR1: View, FlowRepresentable, Inspectable { var _workflowPointer: AnyFlowRepresentable? @@ -57,8 +189,8 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let expectOnFinish1 = expectation(description: "OnFinish1 called") let expectOnFinish2 = expectation(description: "OnFinish2 called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) + WorkflowView { + WorkflowItem(FR1.self) } .onFinish { _ in expectOnFinish1.fulfill() @@ -88,10 +220,9 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let expectOnFinish1 = expectation(description: "OnFinish1 called") let expectOnFinish2 = expectation(description: "OnFinish2 called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: TestUtils.showWorkflow) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(isLaunched: TestUtils.showWorkflow) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } .onFinish { _ in TestUtils.showWorkflow.wrappedValue = false @@ -128,8 +259,8 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } let expected = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expected) { - thenProceed(with: FR1.self) + WorkflowView(isLaunched: .constant(true), launchingWith: expected) { + WorkflowItem(FR1.self) } }.hostAndInspect(with: \.inspection) @@ -147,10 +278,9 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } let expected = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expected) { - thenProceed(with: FR1.self) { - thenProceed(with: FR1.self) - } + WorkflowView(isLaunched: .constant(true), launchingWith: expected) { + WorkflowItem(FR1.self) + WorkflowItem(FR1.self) } }.hostAndInspect(with: \.inspection) @@ -169,10 +299,9 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } let expected = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: AnyWorkflow.PassedArgs.args(expected)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR1.self) - } + WorkflowView(isLaunched: .constant(true), launchingWith: AnyWorkflow.PassedArgs.args(expected)) { + WorkflowItem(FR1.self) + WorkflowItem(FR1.self) } }.hostAndInspect(with: \.inspection) @@ -213,12 +342,10 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let expectedEnd = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedFR1) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) - } - } + WorkflowView(isLaunched: .constant(true), launchingWith: expectedFR1) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) } .onFinish { XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedEnd) @@ -264,20 +391,14 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) { - thenProceed(with: FR5.self) { - thenProceed(with: FR6.self) { - thenProceed(with: FR7.self) - } - } - } - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) } }.hostAndInspect(with: \.inspection) @@ -304,14 +425,11 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { var body: some View { Text("FR3 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR2.self) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR2.self) } }.hostAndInspect(with: \.inspection) @@ -340,14 +458,11 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { var body: some View { Text("FR4 type") } } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) { - thenProceed(with: FR4.self) - } - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) } }.hostAndInspect(with: \.inspection) @@ -369,8 +484,9 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let isLaunched = Binding(wrappedValue: true) let expectOnAbandon = expectation(description: "OnAbandon called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: isLaunched) { - thenProceed(with: FR1.self)} + WorkflowView(isLaunched: isLaunched) { + WorkflowItem(FR1.self) + } .onAbandon { XCTAssertFalse(isLaunched.wrappedValue) expectOnAbandon.fulfill() @@ -394,8 +510,8 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let expectOnAbandon2 = expectation(description: "OnAbandon2 called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: isLaunched) { - thenProceed(with: FR1.self) + WorkflowView(isLaunched: isLaunched) { + WorkflowItem(FR1.self) } .onAbandon { XCTAssertFalse(isLaunched.wrappedValue) @@ -421,8 +537,8 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self).applyModifiers { $0.customModifier().padding().onAppear { } } + WorkflowView { + WorkflowItem(FR1.self).applyModifiers { $0.customModifier().padding().onAppear { } } } }.hostAndInspect(with: \.inspection) @@ -448,10 +564,9 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: TestUtils.binding) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(isLaunched: TestUtils.binding) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } }.hostAndInspect(with: \.inspection) @@ -484,10 +599,9 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let onFinishCalled = expectation(description: "onFinish Called") let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } .onFinish { _ in onFinishCalled.fulfill() @@ -522,10 +636,9 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let expectOnFinish = expectation(description: "OnFinish called") let expectedArgs = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: expectedArgs) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) - } + WorkflowView(isLaunched: .constant(true), launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) } .onFinish { _ in expectOnFinish.fulfill() @@ -559,8 +672,8 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let expectOnFinish = expectation(description: "OnFinish called") let expectedArgs = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true), startingArgs: AnyWorkflow.PassedArgs.args(expectedArgs)) { - thenProceed(with: FR1.self) + WorkflowView(isLaunched: .constant(true), launchingWith: AnyWorkflow.PassedArgs.args(expectedArgs)) { + WorkflowItem(FR1.self) } .onFinish { _ in expectOnFinish.fulfill() @@ -600,12 +713,10 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let expectOnFinish = expectation(description: "OnFinish called") let expectedArgs = UUID().uuidString let launcher = try await MainActor.run { - WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) { - thenProceed(with: FR2.self) { - thenProceed(with: FR3.self) - } - } + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) } .onFinish { _ in expectOnFinish.fulfill() @@ -631,15 +742,14 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { } } - let workflowView = WorkflowLauncher(isLaunched: .constant(true)) { - thenProceed(with: FR1.self) + let workflowView = WorkflowView { + WorkflowItem(FR1.self) } - typealias WorkflowViewContent = State> + typealias WorkflowViewContent = State, Never>>> let content = try XCTUnwrap(Mirror(reflecting: workflowView).descendant("_content") as? WorkflowViewContent) - // Note: Only add to these exceptions if you are *certain* the property should not be @State. Err on the side of the property being @State - let exceptions = ["_model", "_launcher", "_location", "_value", "inspection", "_presentation"] + let exceptions = ["_model", "_launcher", "_location", "_value", "inspection", "_presentation", "_isLaunched"] let mirror = Mirror(reflecting: content.wrappedValue) @@ -666,8 +776,8 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { var body: some View { VStack { Button("") { showingWorkflow = true } - WorkflowLauncher(isLaunched: $showingWorkflow) { - thenProceed(with: FR1.self) + WorkflowView(isLaunched: $showingWorkflow) { + WorkflowItem(FR1.self) } } .onReceive(inspection.notice) { inspection.visit(self, $0) } @@ -677,10 +787,10 @@ final class SwiftCurrent_SwiftUIConsumerTests: XCTestCase, App { let wrapper = try await MainActor.run { Wrapper() }.hostAndInspect(with: \.inspection) let stack = try wrapper.vStack() - let launcher = try stack.view(WorkflowLauncher>.self, 1) - XCTAssertThrowsError(try launcher.view(WorkflowItem.self)) + let launcher = try stack.view(WorkflowView, Never>>>.self, 1).view(WorkflowLauncher, Never>>.self) + XCTAssertThrowsError(try launcher.view(WorkflowItemWrapper, Never>.self)) XCTAssertNoThrow(try stack.button(0).tap()) - XCTAssertNoThrow(try launcher.view(WorkflowItem.self)) + XCTAssertNoThrow(try launcher.view(WorkflowItemWrapper, Never>.self)) } } diff --git a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUI_WorkflowBuilderArityTests.swift b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUI_WorkflowBuilderArityTests.swift new file mode 100644 index 000000000..9db8f6df9 --- /dev/null +++ b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUI_WorkflowBuilderArityTests.swift @@ -0,0 +1,949 @@ +// +// SwiftCurrent_SwiftUI_WorkflowBuilderArityTests.swift +// SwiftCurrent +// +// Created by Matt Freiburg on 2/21/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import XCTest +import SwiftUI +import ViewInspector + +import SwiftCurrent +@testable import SwiftCurrent_SwiftUI // testable sadly needed for inspection.inspect to work + +@available(iOS 15.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +final class SwiftCurrent_SwiftUI_WorkflowBuilderArityTests: XCTestCase, App { + func testArity1() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + } + + func testArity2() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + } + + func testArity3() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + } + + func testArity3_WithBuildOptional() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if true { + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + } + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + } + + func testArity3_WithBuildEither() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + if true { + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + } else { + WorkflowItem(FR3.self) + WorkflowItem(FR2.self) + } + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + } + + func testArity4() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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 viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + } + + func testArity5() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + } + + func testArity6() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + try await viewUnderTest.find(FR6.self).proceedInWorkflow() + } + + func testArity7() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR7: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + try await viewUnderTest.find(FR6.self).proceedInWorkflow() + try await viewUnderTest.find(FR7.self).proceedInWorkflow() + } + + func testArity8() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR7: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR8: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) + WorkflowItem(FR8.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + try await viewUnderTest.find(FR6.self).proceedInWorkflow() + try await viewUnderTest.find(FR7.self).proceedInWorkflow() + try await viewUnderTest.find(FR8.self).proceedInWorkflow() + } + + func testArity9() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR7: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR8: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR9: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) + WorkflowItem(FR8.self) + WorkflowItem(FR9.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + try await viewUnderTest.find(FR6.self).proceedInWorkflow() + try await viewUnderTest.find(FR7.self).proceedInWorkflow() + try await viewUnderTest.find(FR8.self).proceedInWorkflow() + try await viewUnderTest.find(FR9.self).proceedInWorkflow() + } + + func testArity10() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR7: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR8: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR9: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR10: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) + WorkflowItem(FR8.self) + WorkflowItem(FR9.self) + WorkflowItem(FR10.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + try await viewUnderTest.find(FR6.self).proceedInWorkflow() + try await viewUnderTest.find(FR7.self).proceedInWorkflow() + try await viewUnderTest.find(FR8.self).proceedInWorkflow() + try await viewUnderTest.find(FR9.self).proceedInWorkflow() + try await viewUnderTest.find(FR10.self).proceedInWorkflow() + } + + func testArity5_WithWorkflowGroup() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR7: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR8: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR9: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR10: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowGroup { + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) + WorkflowItem(FR8.self) + WorkflowItem(FR9.self) + WorkflowItem(FR10.self) + } + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + try await viewUnderTest.find(FR6.self).proceedInWorkflow() + try await viewUnderTest.find(FR7.self).proceedInWorkflow() + try await viewUnderTest.find(FR8.self).proceedInWorkflow() + try await viewUnderTest.find(FR9.self).proceedInWorkflow() + try await viewUnderTest.find(FR10.self).proceedInWorkflow() + } + + func testUltramassiveWorkflow() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR7: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR8: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR9: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR10: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR11: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR12: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR13: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + struct FR14: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR4 type") } + } + struct FR15: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR16: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR17: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR18: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR19: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR20: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR21: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR22: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR23: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + struct FR24: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR4 type") } + } + struct FR25: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR26: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR27: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR28: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR29: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR30: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR31: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR32: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR33: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR34: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR35: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR36: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR37: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR38: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR39: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR40: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR41: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR42: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR43: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + struct FR44: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR4 type") } + } + struct FR45: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR46: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR47: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR48: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR49: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR50: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR51: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR52: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR53: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + struct FR54: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR4 type") } + } + struct FR55: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR56: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR57: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR58: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR59: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR60: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR61: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR62: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR63: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + struct FR64: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR4 type") } + } + struct FR65: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR66: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR67: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR68: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR8 type") } + } + struct FR69: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR9 type") } + } + struct FR70: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR10 type") } + } + struct FR71: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowGroup { + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) + WorkflowItem(FR8.self) + WorkflowItem(FR9.self) + WorkflowGroup { + WorkflowItem(FR10.self) + WorkflowItem(FR11.self) + WorkflowItem(FR12.self) + WorkflowItem(FR13.self) + WorkflowItem(FR14.self) + WorkflowItem(FR15.self) + WorkflowItem(FR16.self) + WorkflowItem(FR17.self) + WorkflowItem(FR18.self) + WorkflowGroup { + WorkflowItem(FR19.self) + WorkflowItem(FR20.self) + WorkflowItem(FR21.self) + WorkflowItem(FR22.self) + WorkflowItem(FR23.self) + WorkflowItem(FR24.self) + WorkflowItem(FR25.self) + WorkflowItem(FR26.self) + WorkflowItem(FR27.self) + WorkflowGroup { + WorkflowItem(FR28.self) + WorkflowItem(FR29.self) + WorkflowItem(FR30.self) + WorkflowItem(FR31.self) + WorkflowItem(FR32.self) + WorkflowItem(FR33.self) + WorkflowItem(FR34.self) + WorkflowItem(FR35.self) + WorkflowItem(FR36.self) + WorkflowGroup { + WorkflowItem(FR37.self) + WorkflowItem(FR38.self) + WorkflowItem(FR39.self) + WorkflowItem(FR40.self) + WorkflowItem(FR41.self) + WorkflowItem(FR42.self) + WorkflowItem(FR43.self) + WorkflowItem(FR44.self) + WorkflowItem(FR45.self) + WorkflowGroup { + WorkflowItem(FR46.self) + WorkflowItem(FR47.self) + WorkflowItem(FR48.self) + WorkflowItem(FR49.self) + WorkflowItem(FR50.self) + WorkflowItem(FR51.self) + WorkflowItem(FR52.self) + WorkflowItem(FR53.self) + WorkflowGroup { + WorkflowItem(FR54.self) + WorkflowItem(FR55.self) + WorkflowItem(FR56.self) + WorkflowItem(FR57.self) + WorkflowItem(FR58.self) + WorkflowItem(FR59.self) + WorkflowItem(FR60.self) + WorkflowItem(FR61.self) + WorkflowItem(FR62.self) + WorkflowGroup { + WorkflowItem(FR63.self) + WorkflowItem(FR64.self) + WorkflowItem(FR65.self) + WorkflowItem(FR66.self) + WorkflowItem(FR67.self) + WorkflowItem(FR68.self) + WorkflowItem(FR69.self) + WorkflowItem(FR70.self) + WorkflowItem(FR71.self) + } + } + } + } + } + } + } + } + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await MainActor.run { + try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow() + try viewUnderTest.find(ViewType.View.self, traversal: .depthFirst).actualView().proceedInWorkflow() + try viewUnderTest.find(ViewType.View.self, traversal: .depthFirst).actualView().proceedInWorkflow() + } + } +} diff --git a/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUI_WorkflowBuilderTests.swift b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUI_WorkflowBuilderTests.swift new file mode 100644 index 000000000..150234dcb --- /dev/null +++ b/Tests/SwiftCurrent_SwiftUITests/SwiftCurrent_SwiftUI_WorkflowBuilderTests.swift @@ -0,0 +1,678 @@ +// +// SwiftCurrent_SwiftUI_WorkflowBuilderTests.swift +// SwiftCurrent +// +// Created by Tyler Thompson on 2/21/22. +// Copyright © 2022 WWT and Tyler Thompson. All rights reserved. +// + +import XCTest +import SwiftUI +import ViewInspector + +import SwiftCurrent +@testable import SwiftCurrent_SwiftUI // testable sadly needed for inspection.inspect to work + +@available(iOS 15.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +final class SwiftCurrent_SwiftUI_WorkflowBuilderTests: XCTestCase, App { + func testWorkflowCanBeFollowed() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + XCTAssertEqual(try viewUnderTest.find(FR2.self).text().string(), "FR2 type") + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanHaveMultipleOnFinishClosures() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish1 = expectation(description: "OnFinish1 called") + let expectOnFinish2 = expectation(description: "OnFinish2 called") + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + } + .onFinish { _ in + expectOnFinish1.fulfill() + }.onFinish { _ in + expectOnFinish2.fulfill() + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + wait(for: [expectOnFinish1, expectOnFinish2], timeout: TestConstant.timeout) + } + + func testWorkflowPassesArgumentsToTheFirstItem() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + let stringProperty: String + init(with string: String) { + self.stringProperty = string + } + var body: some View { Text("FR1 type") } + } + let expected = UUID().uuidString + + let viewUnderTest = try await MainActor.run { + WorkflowView(launchingWith: expected) { + WorkflowItem(FR1.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).actualView().stringProperty, expected) + } + + func testWorkflowPassesArgumentsToTheFirstItem_WhenThatFirstItemTakesInAnyWorkflowPassedArgs() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + let property: AnyWorkflow.PassedArgs + init(with: AnyWorkflow.PassedArgs) { + self.property = with + } + var body: some View { Text("FR1 type") } + } + let expected = UUID().uuidString + + let viewUnderTest = try await MainActor.run { + WorkflowView(launchingWith: expected) { + WorkflowItem(FR1.self) + WorkflowItem(FR1.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).actualView().property.extractArgs(defaultValue: nil) as? String, expected) + } + + func testWorkflowPassesArgumentsToTheFirstItem_WhenThatFirstItemTakesInAnyWorkflowPassedArgs_AndTheLaunchArgsAreAnyWorkflowPassedArgs() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = AnyWorkflow.PassedArgs + var _workflowPointer: AnyFlowRepresentable? + let property: AnyWorkflow.PassedArgs + init(with: AnyWorkflow.PassedArgs) { + self.property = with + } + var body: some View { Text("FR1 type") } + } + let expected = UUID().uuidString + + let viewUnderTest = try await MainActor.run { + WorkflowView(launchingWith: AnyWorkflow.PassedArgs.args(expected)) { + WorkflowItem(FR1.self) + WorkflowItem(FR1.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).actualView().property.extractArgs(defaultValue: nil) as? String, expected) + } + + func testWorkflowPassesArgumentsToAllItems() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = Int + var _workflowPointer: AnyFlowRepresentable? + let property: String + init(with: String) { + self.property = with + } + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = Bool + var _workflowPointer: AnyFlowRepresentable? + let property: Int + init(with: Int) { + self.property = with + } + var body: some View { Text("FR1 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = String + var _workflowPointer: AnyFlowRepresentable? + let property: Bool + init(with: Bool) { + self.property = with + } + var body: some View { Text("FR1 type") } + } + let expectedFR1 = UUID().uuidString + let expectedFR2 = Int.random(in: 1...10) + let expectedFR3 = Bool.random() + let expectedEnd = UUID().uuidString + + let viewUnderTest = try await MainActor.run { + WorkflowView(launchingWith: expectedFR1) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + } + .onFinish { + XCTAssertEqual($0.extractArgs(defaultValue: nil) as? String, expectedEnd) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).actualView().property, expectedFR1) + XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow(expectedFR2)) + XCTAssertEqual(try viewUnderTest.find(FR2.self).actualView().property, expectedFR2) + XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow(expectedFR3)) + XCTAssertEqual(try viewUnderTest.find(FR3.self).actualView().property, expectedFR3) + XCTAssertNoThrow(try viewUnderTest.find(FR3.self).actualView().proceedInWorkflow(expectedEnd)) + } + + func testLargeWorkflowCanBeFollowed() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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") } + } + struct FR5: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR5 type") } + } + struct FR6: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR6 type") } + } + struct FR7: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR8: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR9: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + struct FR10: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR7 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + WorkflowItem(FR5.self) + WorkflowItem(FR6.self) + WorkflowItem(FR7.self) + WorkflowItem(FR8.self) + WorkflowItem(FR9.self) + WorkflowItem(FR10.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + try await viewUnderTest.find(FR5.self).proceedInWorkflow() + try await viewUnderTest.find(FR6.self).proceedInWorkflow() + try await viewUnderTest.find(FR7.self).proceedInWorkflow() + try await viewUnderTest.find(FR8.self).proceedInWorkflow() + try await viewUnderTest.find(FR9.self).proceedInWorkflow() + try await viewUnderTest.find(FR10.self).proceedInWorkflow() + } + + func testWorkflowOnlyShowsOneViewAtATime() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + struct FR3: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR3 type") } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR2.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + XCTAssertThrowsError(try viewUnderTest.find(FR1.self)) + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + XCTAssertThrowsError(try viewUnderTest.find(FR2.self)) + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + XCTAssertThrowsError(try viewUnderTest.find(FR3.self)) + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + XCTAssertThrowsError(try viewUnderTest.find(ViewType.Text.self, skipFound: 1)) + } + + func testMovingBiDirectionallyInAWorkflow() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + 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 viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + WorkflowItem(FR4.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).backUpInWorkflow() + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).backUpInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + try await viewUnderTest.find(FR4.self).proceedInWorkflow() + } + + func testWorkflowSetsBindingBooleanToFalseWhenAbandoned() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + let isLaunched = Binding(wrappedValue: true) + let expectOnAbandon = expectation(description: "OnAbandon called") + + let viewUnderTest = try await MainActor.run { + WorkflowView(isLaunched: isLaunched) { + WorkflowItem(FR1.self) + }.onAbandon { + XCTAssertFalse(isLaunched.wrappedValue) + expectOnAbandon.fulfill() + } + }.hostAndInspect(with: \.inspection) + + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + try await viewUnderTest.find(FR1.self).abandonWorkflow() + XCTAssertThrowsError(try viewUnderTest.find(FR1.self)) + + wait(for: [expectOnAbandon], timeout: TestConstant.timeout) + } + + func testWorkflowCanHaveMultipleOnAbandonCallbacks() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + let isLaunched = Binding(wrappedValue: true) + let expectOnAbandon1 = expectation(description: "OnAbandon1 called") + let expectOnAbandon2 = expectation(description: "OnAbandon2 called") + + let viewUnderTest = try await MainActor.run { + WorkflowView(isLaunched: isLaunched) { + WorkflowItem(FR1.self) + } + .onAbandon { + XCTAssertFalse(isLaunched.wrappedValue) + expectOnAbandon1.fulfill() + }.onAbandon { + XCTAssertFalse(isLaunched.wrappedValue) + expectOnAbandon2.fulfill() + } + }.hostAndInspect(with: \.inspection) + + try await viewUnderTest.find(FR1.self).abandonWorkflow() + XCTAssertThrowsError(try viewUnderTest.find(FR1.self)) + wait(for: [expectOnAbandon1, expectOnAbandon2], timeout: TestConstant.timeout) + } + + func testWorkflowCanHaveModifiers() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + + func customModifier() -> Self { self } + } + + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + .applyModifiers { + $0.customModifier().padding().onAppear { } + } + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssert(try viewUnderTest.find(FR1.self).hasPadding()) + XCTAssertNoThrow(try viewUnderTest.find(FR1.self).callOnAppear()) + } + + func testWorkflowRelaunchesWhenSubsequentlyLaunched() async throws { + throw XCTSkip("We are currently unable to test this because of a limitation in ViewInspector, see here: https://github.com/nalexn/ViewInspector/issues/126") + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + + func customModifier() -> Self { self } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + + let binding = Binding(wrappedValue: true) + + let viewUnderTest = try await MainActor.run { + WorkflowView(isLaunched: binding) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + + binding.wrappedValue = false + XCTAssertThrowsError(try viewUnderTest.find(FR1.self)) + XCTAssertThrowsError(try viewUnderTest.find(FR2.self)) + + binding.wrappedValue = true + XCTAssertNoThrow(try viewUnderTest.callOnChange(newValue: false)) + XCTAssertNoThrow(try viewUnderTest.find(FR1.self)) + XCTAssertThrowsError(try viewUnderTest.find(FR2.self)) + } + + func testWorkflowRelaunchesWhenAbandoned_WithAConstantOfTrue() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + struct FR2: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + + func abandon() { + workflow?.abandon() + } + } + let onFinishCalled = expectation(description: "onFinish Called") + + let viewUnderTest = try await MainActor.run { + WorkflowView(isLaunched: .constant(true)) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + } + .onFinish { _ in + onFinishCalled.fulfill() + } + }.hostAndInspect(with: \.inspection) + + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().abandon()) + XCTAssertThrowsError(try viewUnderTest.find(FR2.self)) + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + + wait(for: [onFinishCalled], timeout: TestConstant.timeout) + } + + func testWorkflowCanHaveAPassthroughRepresentable() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + typealias WorkflowOutput = AnyWorkflow.PassedArgs + var _workflowPointer: AnyFlowRepresentable? + private let data: AnyWorkflow.PassedArgs + var body: some View { Text("FR1 type") } + + init(with data: AnyWorkflow.PassedArgs) { + self.data = data + } + } + struct FR2: View, FlowRepresentable, Inspectable { + init(with str: String) { } + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let expectedArgs = UUID().uuidString + + let viewUnderTest = try await MainActor.run { + WorkflowView(isLaunched: .constant(true), launchingWith: expectedArgs) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + XCTAssertNoThrow(try viewUnderTest.find(FR1.self).actualView().proceedInWorkflow(.args(expectedArgs))) + XCTAssertEqual(try viewUnderTest.find(FR2.self).text().string(), "FR2 type") + try await viewUnderTest.find(FR2.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanConvertAnyArgsToCorrectTypeForFirstItem() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + let data: String + + var body: some View { Text("FR1 type") } + + init(with data: String) { + self.data = data + } + } + struct FR2: View, FlowRepresentable, Inspectable { + init(with str: String) { } + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR2 type") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let expectedArgs = UUID().uuidString + + let viewUnderTest = try await MainActor.run { + WorkflowView(isLaunched: .constant(true), + launchingWith: AnyWorkflow.PassedArgs.args(expectedArgs)) { + WorkflowItem(FR1.self) + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + XCTAssertEqual(try viewUnderTest.find(FR1.self).actualView().data, expectedArgs) + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCanHaveAPassthroughRepresentableInTheMiddle() async throws { + 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)") } + } + let expectOnFinish = expectation(description: "OnFinish called") + let expectedArgs = UUID().uuidString + + let viewUnderTest = try await MainActor.run { + WorkflowView(isLaunched: .constant(true)) { + WorkflowItem(FR1.self) + WorkflowItem(FR2.self) + WorkflowItem(FR3.self) + } + .onFinish { _ in + expectOnFinish.fulfill() + } + }.hostAndInspect(with: \.inspection).extractWorkflowLauncher().extractWorkflowItemWrapper() + + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + try await viewUnderTest.find(FR1.self).proceedInWorkflow() + XCTAssertEqual(try viewUnderTest.find(FR2.self).text().string(), "FR2 type") + XCTAssertNoThrow(try viewUnderTest.find(FR2.self).actualView().proceedInWorkflow(.args(expectedArgs))) + XCTAssertEqual(try viewUnderTest.find(FR3.self).text().string(), "FR3 type, \(expectedArgs)") + try await viewUnderTest.find(FR3.self).proceedInWorkflow() + + wait(for: [expectOnFinish], timeout: TestConstant.timeout) + } + + func testWorkflowCorrectlyHandlesState() async throws { + struct FR1: View, FlowRepresentable { + weak var _workflowPointer: AnyFlowRepresentable? + + var body: some View { + Button("Proceed") { proceedInWorkflow() } + } + } + + let workflowView = await MainActor.run { + WorkflowView(isLaunched: .constant(true)) { + WorkflowItem(FR1.self) + } + } + + typealias WorkflowViewContent = State, Never>>> + _ = try XCTUnwrap(Mirror(reflecting: workflowView).descendant("_content") as? WorkflowViewContent) + } + + func testWorkflowCanHaveADelayedLaunch() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + weak var _workflowPointer: AnyFlowRepresentable? + + var body: some View { + Button("Proceed") { proceedInWorkflow() } + } + } + + struct Wrapper: View, Inspectable { + @State var showingWorkflow = false + let inspection = Inspection() + var body: some View { + VStack { + Button("") { showingWorkflow = true } + WorkflowView(isLaunched: $showingWorkflow) { + WorkflowItem(FR1.self) + } + } + .onReceive(inspection.notice) { inspection.visit(self, $0) } + } + } + + let view = try await MainActor.run { Wrapper() }.hostAndInspect(with: \.inspection) + let stack = try view.vStack() + let workflowView = try stack.view(WorkflowView, Never>>>.self, 1) + let launcher = try workflowView.view(WorkflowLauncher, Never>>.self) + + XCTAssertThrowsError(try launcher.view(WorkflowItemWrapper, Never>.self)) + XCTAssertNoThrow(try stack.button(0).tap()) + XCTAssertNoThrow(try launcher.view(WorkflowItemWrapper, Never>.self)) + } + + func testWorkflowCanBeEmbeddedInNavView() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("FR1 type") } + } + let viewUnderTest = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + }.embedInNavigationView() + }.hostAndInspect(with: \.inspection) + + XCTAssertNoThrow(try viewUnderTest.view(WorkflowLauncher>.self).navigationView()) + XCTAssertEqual(try viewUnderTest.find(FR1.self).text().string(), "FR1 type") + } + + func testWorkflowCanBeLaunched_WithoutArguments_WhenInputIsAnyWorkflowPassedArgs() async throws { + struct FR1: View, FlowRepresentable, Inspectable { + typealias WorkflowInput = AnyWorkflow.PassedArgs + let input: AnyWorkflow.PassedArgs + init(with args: AnyWorkflow.PassedArgs) { + input = args + } + + var _workflowPointer: AnyFlowRepresentable? + var body: some View { Text("I'm dropping these args on the flo") } + } + + let view = try await MainActor.run { + WorkflowView { + WorkflowItem(FR1.self) + } + }.hostAndInspect(with: \.inspection) + + XCTAssertNoThrow(try view.find(FR1.self)) + let input = try view.find(FR1.self).actualView().input + guard case AnyWorkflow.PassedArgs.none = input else { + XCTFail("We expected AnyWorkflow.PassedArgs to be .none, but it was \(input)") + return + } + } +} diff --git a/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift b/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift index e14f62d80..c53f1db5a 100644 --- a/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift +++ b/Tests/SwiftCurrent_SwiftUITests/ViewInspector/InspectableExtensions.swift @@ -15,9 +15,23 @@ 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 WorkflowItemWrapper: 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 WorkflowView: Inspectable { } +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension WorkflowGroup: Inspectable { } +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension OptionalWorkflowItem: Inspectable { } +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension EitherWorkflowItem: Inspectable { } +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension EitherWorkflowItem.Either: Inspectable { } +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) extension ViewControllerWrapper: Inspectable { } +@available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) +extension ModalModifier: Inspectable { } @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) extension Inspection: InspectionEmissary where V: View { } diff --git a/Tests/SwiftCurrent_SwiftUITests/ViewInspector/ViewHostingExtensions.swift b/Tests/SwiftCurrent_SwiftUITests/ViewInspector/ViewHostingExtensions.swift index 15f91d8af..1e97a706f 100644 --- a/Tests/SwiftCurrent_SwiftUITests/ViewInspector/ViewHostingExtensions.swift +++ b/Tests/SwiftCurrent_SwiftUITests/ViewInspector/ViewHostingExtensions.swift @@ -15,12 +15,14 @@ import SwiftCurrent @available(iOS 15.0, macOS 10.15, tvOS 13.0, *) extension View where Self: Inspectable { - func host() async { + @discardableResult func host() async -> Self { await MainActor.run { ViewHosting.host(view: self ) } + return self } - func host(_ transform: (Self) -> V) async { + @discardableResult func host(_ transform: (Self) -> V) async -> Self { await MainActor.run { ViewHosting.host(view: transform(self) ) } + return self } func hostAndInspect(with emissary: KeyPath) async throws -> InspectableView> where E.V == Self { @@ -31,11 +33,21 @@ extension View where Self: Inspectable { @available(iOS 15.0, macOS 11, tvOS 14.0, watchOS 7.0, *) extension InspectableView where View: CustomViewType & SingleViewContent { - func extractWorkflowItem() async throws -> InspectableView>> where View.T == WorkflowLauncher> { + func extractWorkflowLauncher() async throws -> InspectableView>> where View.T == WorkflowView> { + let actual = try view(WorkflowLauncher.self).actualView() + + DispatchQueue.main.async { + ViewHosting.host(view: actual) + } + + return try await actual.inspection.inspect() + } + + func extractWorkflowItemWrapper() async throws -> InspectableView>> where View.T == WorkflowLauncher> { let mirror = Mirror(reflecting: try actualView()) let model = try XCTUnwrap(mirror.descendant("_model") as? StateObject) let launcher = try XCTUnwrap(mirror.descendant("_launcher") as? StateObject) - let actual = try view(WorkflowItem.self).actualView() + let actual = try view(WorkflowItemWrapper.self).actualView() DispatchQueue.main.async { ViewHosting.host(view: actual @@ -46,7 +58,7 @@ extension InspectableView where View: CustomViewType & SingleViewContent { return try await actual.inspection.inspect() } - func extractWrappedWorkflowItem() async throws -> InspectableView>> where View.T == WorkflowItem, PC> { + func extractWrappedWrapper() async throws -> InspectableView>> where View.T == WorkflowItemWrapper> { let wrapped = try await actualView().getWrappedView() let mirror = Mirror(reflecting: try actualView()) let model = try XCTUnwrap(mirror.descendant("_model") as? EnvironmentObject) @@ -58,6 +70,10 @@ extension InspectableView where View: CustomViewType & SingleViewContent { } return try await wrapped.inspection.inspect() } + + func findModalModifier() throws -> InspectableView>> where View.T == WorkflowItemWrapper { + try find(ModalModifier.self) + } } @available(iOS 15.0, macOS 11, tvOS 14.0, watchOS 7.0, *) diff --git a/Tests/SwiftCurrent_SwiftUITests/WorkflowItemExtensions.swift b/Tests/SwiftCurrent_SwiftUITests/WorkflowItemExtensions.swift index e8dc7339b..1507f20fc 100644 --- a/Tests/SwiftCurrent_SwiftUITests/WorkflowItemExtensions.swift +++ b/Tests/SwiftCurrent_SwiftUITests/WorkflowItemExtensions.swift @@ -13,7 +13,7 @@ import ViewInspector @testable import SwiftCurrent_SwiftUI @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *) -extension WorkflowItem { +extension WorkflowItemWrapper { func getWrappedView() throws -> Wrapped { try XCTUnwrap((Mirror(reflecting: self).descendant("_wrapped") as? State)?.wrappedValue) }