Skip to content

Commit

Permalink
Merge pull request #179 from wwt/workflow-builder
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick Kaczmarek committed Mar 21, 2022
2 parents 5ad629b + 94f06fe commit 40432d4
Show file tree
Hide file tree
Showing 58 changed files with 4,506 additions and 2,068 deletions.
6 changes: 2 additions & 4 deletions .github/.jazzy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions .github/UPGRADE_PATH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details>
<summary><b>V4 -> V5</b></summary>

## 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")`
</details>

---

<details>
<summary><b>V3 -> V4</b></summary>

Expand Down
2 changes: 1 addition & 1 deletion .github/abstract/Controlling Presentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 10 additions & 7 deletions .github/abstract/Creating Workflows in SwiftUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
```
2 changes: 1 addition & 1 deletion .github/abstract/Creating Workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
8 changes: 4 additions & 4 deletions .github/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,22 @@ 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,
strict: true
)
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"
Expand Down
28 changes: 14 additions & 14 deletions .github/guides/Getting Started with SwiftUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -152,21 +153,21 @@ struct Content_Previews: PreviewProvider {

<details>

In SwiftUI, the <code>Workflow</code> type is handled by the library when you start with a <code>WorkflowLauncher</code>.
In SwiftUI, the <code>Workflow</code> type is handled by the library when you start with a <code>WorkflowView</code>.
</details>

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

<details>

<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>.
<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>.
</details>

#### **What's going on with this `startingArgs` and `passedArgs`?**
#### **What's going on with this `launchingWith` and `passedArgs`?**

<details>

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

<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.
</details>
Expand Down Expand Up @@ -205,9 +206,8 @@ final class FirstViewController: UIWorkflowItem<Never, Never>, 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
}
```
18 changes: 6 additions & 12 deletions .github/guides/Working with Modals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
```

Expand All @@ -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))
}
```
26 changes: 13 additions & 13 deletions .github/guides/Working with NavigationView.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
```
Expand All @@ -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()
```

Expand All @@ -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
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -112,6 +114,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsOnboardingViewController.swift; sourceTree = "<group>"; };
628952DB27E528CC00FDDCEF /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
CA0536F526A0888200BF8FC5 /* ProfileFeatureOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFeatureOnboardingView.swift; sourceTree = "<group>"; };
CA238D1326A1153B000A36EC /* ContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewTests.swift; sourceTree = "<group>"; };
CA4A6F1F26CDAEE600BE3E74 /* TestEventReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestEventReceiver.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -231,6 +235,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
628952D827E5280000FDDCEF /* Settings */ = {
isa = PBXGroup;
children = (
628952D927E5281700FDDCEF /* SettingsOnboardingViewController.swift */,
628952DB27E528CC00FDDCEF /* SettingsViewController.swift */,
);
path = Settings;
sourceTree = "<group>";
};
CA0536F926A0917A00BF8FC5 /* Profile */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -334,6 +347,7 @@
CAC34B6D26A07FE90039A373 /* Views */ = {
isa = PBXGroup;
children = (
628952D827E5280000FDDCEF /* Settings */,
D72B763526FBCF5A00E0405F /* Design */,
D78139CB270DE3AD004A4721 /* Map */,
CA0536F926A0917A00BF8FC5 /* Profile */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 40432d4

Please sign in to comment.