Skip to content

Commit 40432d4

Browse files
author
Nick Kaczmarek
authored
Merge pull request #179 from wwt/workflow-builder
2 parents 5ad629b + 94f06fe commit 40432d4

58 files changed

Lines changed: 4506 additions & 2068 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/.jazzy.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@ custom_categories:
2727
- Working with Modals
2828
- name: Creating Workflows in SwiftUI
2929
children:
30-
- WorkflowLauncher
30+
- WorkflowView
3131
- WorkflowItem
32-
- View
33-
- App
34-
- Scene
32+
- WorkflowBuilder
3533
- name: How to use SwiftCurrent with UIKit
3634
children:
3735
- Using Programmatic Views

.github/UPGRADE_PATH.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@ Use this document to help you understand how to update between major versions of
33

44
Our directions are written for only 1 major version upgrade at a time, as we have found that to be the best experience.
55

6+
<details>
7+
<summary><b>V4 -> V5</b></summary>
8+
9+
## SwiftUI - WorkflowView
10+
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`.
11+
12+
```swift
13+
WorkflowView {
14+
WorkflowItem(FirstView.self) // This view is shown first
15+
WorkflowItem(SecondView.self) // After proceeding, this view is shown
16+
}
17+
```
18+
19+
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")`.
20+
21+
`WorkflowView`'s initializer defaults `isLaunched` to `.constant(true)` meaning you can exclude that parameter and just use `WorkflowView(launchingWith: "someArgs")`
22+
</details>
23+
24+
---
25+
626
<details>
727
<summary><b>V3 -> V4</b></summary>
828

.github/abstract/Controlling Presentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ SwiftCurrent allows you to control how your workflow presents its `FlowRepresent
44
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`.
55

66
### In SwiftUI
7-
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`.
7+
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`.
88

99
### Persistence
1010
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.

.github/abstract/Creating Workflows in SwiftUI.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@ struct FirstView: View, FlowRepresentable {
2222
> **Note:** `FlowRepresentable.proceedInWorkflow()` is what you call to have your view move forward to the next item in the `Workflow` it is part of.
2323
2424
### Step 2:
25-
Define your `WorkflowLauncher`. This indicates if the workflow is shown and describes what items are in it.
25+
Define your `WorkflowView`. This indicates if the workflow is shown and describes what items are in it.
2626

2727
#### Example:
2828
```swift
29-
WorkflowLauncher(isLaunched: .constant(true)) { // Could also have been $someStateOrBindingBoolean
30-
thenProceed(with: FirstView.self) { // thenProceed is a function to create a `WorkflowItem`
31-
thenProceed(with: SecondView.self) { // Use closures to define what comes next
32-
thenProceed(with: ThirdView.self) // The final item needs no closures
33-
}
34-
}
29+
/*
30+
Each item in the workflow is defined as a `WorkflowItem`
31+
passing the type of the FlowRepresentable to create
32+
when appropriate as the workflow proceeds
33+
*/
34+
WorkflowView {
35+
WorkflowItem(FirstView.self)
36+
WorkflowItem(SecondView.self)
37+
WorkflowItem(ThirdView.self)
3538
}
3639
```

.github/abstract/Creating Workflows.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ Workflows enforce (either at compile-time or run-time) that the sequence of `Flo
99

1010
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.
1111

12-
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`.
12+
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`.

.github/fastlane/Fastfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,22 @@ platform :ios do
6868

6969
lane :cocoapods_liblint do
7070
pod_lib_lint(
71-
podspec: '../SwiftCurrent.podspec',
71+
podspec: '../SwiftCurrent.podspec',
7272
allow_warnings: true,
7373
no_clean: true
7474
)
7575
end
7676

77-
lane :lint do
77+
lane :lint do
7878
swiftlint(
7979
config_file: 'SwiftCurrentLint/.swiftlint.yml',
8080
raise_if_swiftlint_error: true,
8181
strict: true
8282
)
8383
end
8484

85-
lane :lintfix do
86-
sh('swiftlint --fix --config=../../SwiftCurrentLint/.swiftlint.yml')
85+
lane :lintfix do
86+
sh('swiftlint --fix --config=../SwiftCurrentLint/.swiftlint.yml')
8787
end
8888

8989
desc "Release a new version with a patch bump_type"

.github/guides/Getting Started with SwiftUI.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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`.
44

5-
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:
5+
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:
66

77
![Preview image of app](https://user-images.githubusercontent.com/79471462/131556533-f2ad1e6c-9acd-4d62-94ac-9140c9718f95.gif)
88

@@ -111,7 +111,7 @@ struct SecondView_Previews: PreviewProvider {
111111

112112
## Launching the `Workflow`
113113

114-
Next we add a `WorkflowLauncher` to the body of our starting app view, in this case `ContentView`.
114+
Next we add a `WorkflowView` to the body of our starting app view, in this case `ContentView`.
115115

116116
```swift
117117
import SwiftUI
@@ -123,10 +123,11 @@ struct ContentView: View {
123123
if !workflowIsPresented {
124124
Button("Present") { workflowIsPresented = true }
125125
} else {
126-
WorkflowLauncher(isLaunched: $workflowIsPresented, startingArgs: "SwiftCurrent") { // SwiftCurrent
127-
thenProceed(with: FirstView.self) { // SwiftCurrent
128-
thenProceed(with: SecondView.self).applyModifiers { $0.padding().border(Color.gray) } // SwiftCurrent
129-
}.applyModifiers { firstView in firstView.padding().border(Color.gray) } // SwiftCurrent
126+
WorkflowView(isLaunched: $workflowIsPresented, launchingWith: "SwiftCurrent") { // SwiftCurrent
127+
WorkflowItem(FirstView.self) // SwiftCurrent
128+
.applyModifiers { firstView in firstView.padding().border(Color.gray) } // SwiftCurrent
129+
WorkflowItem(SecondView.self) // SwiftCurrent
130+
.applyModifiers { $0.padding().border(Color.gray) } // SwiftCurrent
130131
}.onFinish { passedArgs in // SwiftCurrent
131132
workflowIsPresented = false
132133
guard case .args(let emailAddress as String) = passedArgs else {
@@ -152,21 +153,21 @@ struct Content_Previews: PreviewProvider {
152153

153154
<details>
154155

155-
In SwiftUI, the <code>Workflow</code> type is handled by the library when you start with a <code>WorkflowLauncher</code>.
156+
In SwiftUI, the <code>Workflow</code> type is handled by the library when you start with a <code>WorkflowView</code>.
156157
</details>
157158

158159
#### **Where is the type safety I heard about?**
159160

160161
<details>
161162

162-
<code>WorkflowLauncher</code> is specialized with your <code>startingArgs</code> type. <code>FlowRepresentable</code> is specialized with the <code>FlowRepresentable.WorkflowInput</code> and <code>FlowRepresentable.WorkflowOutput</code> associated types. These all work together when creating your flow at run-time to ensure the validity of your <code>Workflow</code>. If the output of <code>FirstView</code> does not match the input of <code>SecondView</code>, the library will send an error when creating the <code>Workflow</code>.
163+
<code>WorkflowView</code> is specialized with your <code>launchingWith</code> type. <code>FlowRepresentable</code> is specialized with the <code>FlowRepresentable.WorkflowInput</code> and <code>FlowRepresentable.WorkflowOutput</code> associated types. These all work together when creating your flow at run-time to ensure the validity of your <code>Workflow</code>. If the output of <code>FirstView</code> does not match the input of <code>SecondView</code>, the library will send an error when creating the <code>Workflow</code>.
163164
</details>
164165

165-
#### **What's going on with this `startingArgs` and `passedArgs`?**
166+
#### **What's going on with this `launchingWith` and `passedArgs`?**
166167

167168
<details>
168169

169-
<code>startingArgs</code> are the <code>AnyWorkflow.PassedArgs</code> handed to the first <code>FlowRepresentable</code> in the workflow. These arguments are used to pass data and determine if the view should load.
170+
<code>launchingWith</code> are the <code>AnyWorkflow.PassedArgs</code> handed to the first <code>FlowRepresentable</code> in the workflow. These arguments are used to pass data and determine if the view should load.
170171

171172
<code>passedArgs</code> are the <code>AnyWorkflow.PassedArgs</code> coming from the last view in the workflow. <code>onFinish</code> is only called when the user has gone through all the screens in the <code>Workflow</code> by navigation or skipping. For this workflow, <code>passedArgs</code> is going to be the output of <code>FirstView</code> or <code>SecondView</code>, depending on the email signature typed in <code>FirstView</code>. To extract the value, we unwrap the variable within the case of <code>.args()</code> as we expect this workflow to return some argument.
172173
</details>
@@ -205,9 +206,8 @@ final class FirstViewController: UIWorkflowItem<Never, Never>, FlowRepresentable
205206
Now in SwiftUI simply reference that controller.
206207

207208
```swift
208-
WorkflowLauncher(isLaunched: $workflowIsPresented) { // SwiftCurrent
209-
thenProceed(with: FirstViewController.self) { // SwiftCurrent
210-
thenProceed(with: SecondView.self) // SwiftCurrent
211-
}
209+
WorkflowView(isLaunched: $workflowIsPresented) { // SwiftCurrent
210+
WorkflowItem(FirstViewController.self) // SwiftCurrent
211+
WorkflowItem(SecondView.self) // SwiftCurrent
212212
}
213213
```

.github/guides/Working with Modals.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ When constructing a workflow, you can use `WorkflowItem.presentationType(_:)` al
33

44
#### Example
55
```swift
6-
NavigationView {
7-
WorkflowLauncher(isLaunched: .constant(true)) {
8-
thenProceed(with: FirstView.self) {
9-
thenProceed(with: SecondView.self).presentationType(.modal)
10-
}
11-
}
6+
WorkflowView {
7+
WorkflowItem(FirstView.self)
8+
WorkflowItem(SecondView.self).presentationType(.modal)
129
}
1310
```
1411

@@ -22,11 +19,8 @@ When you use a presentation type of `LaunchStyle.SwiftUI.PresentationType.modal`
2219
#### Example
2320
The following will use a full-screen cover:
2421
```swift
25-
NavigationView {
26-
WorkflowLauncher(isLaunched: .constant(true)) {
27-
thenProceed(with: FirstView.self) {
28-
thenProceed(with: SecondView.self).presentationType(.modal(.fullScreenCover))
29-
}
30-
}
22+
WorkflowView {
23+
WorkflowItem(FirstView.self)
24+
WorkflowItem(SecondView.self).presentationType(.modal(.fullScreenCover))
3125
}
3226
```

.github/guides/Working with NavigationView.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ When constructing a workflow, you can use `WorkflowItem.presentationType(_:)` al
44
#### Example
55
```swift
66
NavigationView {
7-
WorkflowLauncher(isLaunched: .constant(true)) {
8-
thenProceed(with: FirstView.self) {
9-
thenProceed(with: SecondView.self)
10-
}.presentationType(.navigationLink)
7+
WorkflowView {
8+
WorkflowItem(FirstView.self)
9+
.presentationType(.navigationLink)
10+
WorkflowItem(SecondView.self)
1111
}
1212
}
1313
```
@@ -17,15 +17,15 @@ With that, you've described that `FirstView` should be wrapped in a `NavigationL
1717
> **NOTE:** The `NavigationLink` is in the background of the view to prevent your entire view from being tappable.
1818
1919
### Different NavigationView Styles
20-
SwiftCurrent comes with a convenience function on `WorkflowLauncher` that tries to pick the best `NavigationViewStyle` for a `Workflow`. Normally that's stack-based navigation.
20+
SwiftCurrent comes with a convenience function on `WorkflowView` that tries to pick the best `NavigationViewStyle` for a `Workflow`. Normally that's stack-based navigation.
2121

2222
#### Example
2323
The earlier example could be rewritten as:
2424
```swift
25-
WorkflowLauncher(isLaunched: .constant(true)) {
26-
thenProceed(with: FirstView.self) {
27-
thenProceed(with: SecondView.self)
28-
}.presentationType(.navigationLink)
25+
WorkflowView {
26+
WorkflowItem(FirstView.self)
27+
.presentationType(.navigationLink)
28+
WorkflowItem(SecondView.self)
2929
}.embedInNavigationView()
3030
```
3131

@@ -36,10 +36,10 @@ If you want to use column-based navigation you can simply manage it yourself:
3636
```swift
3737
NavigationView {
3838
FirstColumn() // Could ALSO be a workflow
39-
WorkflowLauncher(isLaunched: .constant(true)) {
40-
thenProceed(with: FirstView.self) {
41-
thenProceed(with: SecondView.self)
42-
}.presentationType(.navigationLink)
39+
WorkflowView {
40+
WorkflowItem(FirstView.self)
41+
.presentationType(.navigationLink)
42+
WorkflowItem(SecondView.self)
4343
} // don't call embedInNavigationView here
4444
}
4545
```

ExampleApps/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
628952DA27E5281700FDDCEF /* SettingsOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */; };
11+
628952DC27E528CC00FDDCEF /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628952DB27E528CC00FDDCEF /* SettingsViewController.swift */; };
1012
CA0536F626A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0536F526A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift */; };
1113
CA238D1426A1153B000A36EC /* ContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA238D1326A1153B000A36EC /* ContentViewTests.swift */; };
1214
CA4A6F2026CDAEE600BE3E74 /* TestEventReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4A6F1F26CDAEE600BE3E74 /* TestEventReceiver.swift */; };
@@ -112,6 +114,8 @@
112114
/* End PBXContainerItemProxy section */
113115

114116
/* Begin PBXFileReference section */
117+
628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsOnboardingViewController.swift; sourceTree = "<group>"; };
118+
628952DB27E528CC00FDDCEF /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
115119
CA0536F526A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFeatureOnboardingView.swift; sourceTree = "<group>"; };
116120
CA238D1326A1153B000A36EC /* ContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewTests.swift; sourceTree = "<group>"; };
117121
CA4A6F1F26CDAEE600BE3E74 /* TestEventReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestEventReceiver.swift; sourceTree = "<group>"; };
@@ -231,6 +235,15 @@
231235
/* End PBXFrameworksBuildPhase section */
232236

233237
/* Begin PBXGroup section */
238+
628952D827E5280000FDDCEF /* Settings */ = {
239+
isa = PBXGroup;
240+
children = (
241+
628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */,
242+
628952DB27E528CC00FDDCEF /* SettingsViewController.swift */,
243+
);
244+
path = Settings;
245+
sourceTree = "<group>";
246+
};
234247
CA0536F926A0917A00BF8FC5 /* Profile */ = {
235248
isa = PBXGroup;
236249
children = (
@@ -334,6 +347,7 @@
334347
CAC34B6D26A07FE90039A373 /* Views */ = {
335348
isa = PBXGroup;
336349
children = (
350+
628952D827E5280000FDDCEF /* Settings */,
337351
D72B763526FBCF5A00E0405F /* Design */,
338352
D78139CB270DE3AD004A4721 /* Map */,
339353
CA0536F926A0917A00BF8FC5 /* Profile */,
@@ -663,6 +677,7 @@
663677
CA7B829F26A1FAAC005AA87D /* InspectableAlert.swift in Sources */,
664678
CA0536F626A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift in Sources */,
665679
CAC34B4326A07F830039A373 /* SwiftUIExampleApp.swift in Sources */,
680+
628952DC27E528CC00FDDCEF /* SettingsViewController.swift in Sources */,
666681
D72B765526FC032B00E0405F /* ChangeEmailView.swift in Sources */,
667682
CA6FB0DE26C6AD5200FB3285 /* UIKitInteropProgrammaticViewController.swift in Sources */,
668683
CA7B821026A123F6005AA87D /* InspectableSheet.swift in Sources */,
@@ -687,6 +702,7 @@
687702
D72B765726FC036A00E0405F /* AccountInformationView.swift in Sources */,
688703
CAC34B7526A07FE90039A373 /* MapFeatureOnboardingView.swift in Sources */,
689704
D72B764926FBCFB200E0405F /* LoginView.swift in Sources */,
705+
628952DA27E5281700FDDCEF /* SettingsOnboardingViewController.swift in Sources */,
690706
D72B764326FBCF7000E0405F /* PasswordField.swift in Sources */,
691707
D7A6CE7E26E039C300599824 /* TestView.swift in Sources */,
692708
D72B764A26FBCFB200E0405F /* SignUp.swift in Sources */,

0 commit comments

Comments
 (0)