Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NavigationLinks are now supported in SwiftUI! #116

Merged
merged 40 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2fe961b
[skip ci] navigation-links - Created a copy of consumer tests and nam…
Tyler-Keith-Thompson Aug 28, 2021
d14432e
[skip ci] navigation-links - inspection extensions now expose errors …
Tyler-Keith-Thompson Aug 28, 2021
5f1123d
[navigation-links] - Discovered an issue with our inspect methods and…
Tyler-Keith-Thompson Aug 28, 2021
62f12da
[navigation-links] - Yup, new testing exposed some important aspects …
Tyler-Keith-Thompson Aug 28, 2021
28f0497
[skip ci] navigation-links - Just fixing the tests that broke due to …
Tyler-Keith-Thompson Aug 28, 2021
8d22770
[navigation-links] - Fixed a test we missed when refactoring all the …
Tyler-Keith-Thompson Aug 28, 2021
fa84239
[navigation-links] - Commented out the bidirectional test that I have…
Tyler-Keith-Thompson Aug 28, 2021
a3d4213
[navigation-links] - Properly failing tests for nav links - TT
Tyler-Keith-Thompson Aug 28, 2021
58c49cd
[navigation-links] - Even *better* tests that can stand up to the rob…
Tyler-Keith-Thompson Aug 29, 2021
74bdd56
[navigation-links] - Added the first of many tests for skipping an it…
Tyler-Keith-Thompson Aug 29, 2021
23e74c5
[skip ci] navigation-links - Added test for skipping the first item i…
Tyler-Keith-Thompson Aug 29, 2021
d33f56b
[skip ci] navigation-links - Tested skipping 2 middle items - TT
Tyler-Keith-Thompson Aug 29, 2021
14f0199
[skip ci] navigation-links - Added test for skipping last item - TT
Tyler-Keith-Thompson Aug 29, 2021
e0d9012
[skip ci] navigation-links - Got a test that moves in both directions…
Tyler-Keith-Thompson Aug 29, 2021
bd14b3c
[skip ci] navigation-links - Wrote a test for persistWhenSkipped, but…
Tyler-Keith-Thompson Aug 30, 2021
4125768
[navigation-links] - Checks that the view to swap is unique - mz tt
morganzellers Aug 30, 2021
b736be4
[navigation-links] - Handling skipping views - mz tt
morganzellers Aug 30, 2021
c142f10
[navigation-links] - Adds backup publisher and handles backups - tt mz
morganzellers Aug 30, 2021
94ca418
[navigation-links] - Extract logic for activating view on backup to f…
morganzellers Aug 30, 2021
3a60bf2
[navigation-links] - Refactoring out ViewBuilder and environment obje…
morganzellers Aug 30, 2021
0291a33
[navigation-links] - Re-enable skipped test that passes - TT
Tyler-Keith-Thompson Aug 30, 2021
ef7cd5b
[navigation-links] - We made backUp work for nav stacks in all versio…
Tyler-Keith-Thompson Aug 30, 2021
21ce3c5
[navigation-links] - Extracting proceed in workflow function - mz tt rag
morganzellers Aug 30, 2021
b6ba808
[navigation-links] - removed persistence tests, we are deferring work…
Tyler-Keith-Thompson Aug 30, 2021
75cb170
[navigation-links] - Extracting function to clear queued expectations…
morganzellers Aug 30, 2021
0a904d0
[navigation-links] - Moving extracted function to XCTestCaseExtension…
morganzellers Aug 30, 2021
f20fc74
[navigation-links] - Added check for following WorkflowItems of all t…
morganzellers Aug 30, 2021
86b0880
[navigation-links] - Removing check for is Content when already check…
Richard-Gist Aug 30, 2021
d4310fb
[navigation-links] - Added a convenience method for embedding into a …
Tyler-Keith-Thompson Aug 30, 2021
61aaf68
Merge branch 'navigation-links' of github.com:wwt/SwiftCurrent into n…
Tyler-Keith-Thompson Aug 30, 2021
eab839b
[navigation-links] - Deleted entitlements, they are not needed - TT
Tyler-Keith-Thompson Aug 30, 2021
3adc0d9
[navigation-links] - Could not figure out how to test the thing Richa…
Tyler-Keith-Thompson Aug 30, 2021
6a7f764
[navigation-links] - sadly, we determined these to be untestable earl…
Tyler-Keith-Thompson Aug 30, 2021
e743dea
[navigation-links] - Added categories and guides to our Jazzy Docs - …
Tyler-Keith-Thompson Aug 31, 2021
76ae76d
[navigation-links] - Updated the guide in SwiftUI on working with Nav…
Tyler-Keith-Thompson Aug 31, 2021
8ac0473
[navigation-links] - Updated the tutorials and docs to point to the c…
Tyler-Keith-Thompson Aug 31, 2021
b0c734c
[navigation-links] - Added an abstract for SwiftUI to jazzy docs - TT
Tyler-Keith-Thompson Aug 31, 2021
75524ab
[navigation-links] - Added an abstract for creating workflows in Swif…
Tyler-Keith-Thompson Aug 31, 2021
04bfc45
Apply suggestions from code review
Tyler-Keith-Thompson Aug 31, 2021
f209260
[navigation-links] - Minor wording tweaks to our abstract on how to u…
Tyler-Keith-Thompson Aug 31, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion .github/.jazzy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,47 @@ module: SwiftCurrent
author_url: https://github.com/wwt/SwiftCurrent
github_url: https://github.com/wwt/SwiftCurrent
sourcekitten_sourcefile: swiftcurrent-docs.json,swiftcurrentuikit-docs.json,swiftcurrent-swiftui-docs.json
author: WWT and Tyler Thompson
author: WWT and Tyler Thompson
documentation: .github/guides/*.md
abstract: .github/abstract/*.md
custom_categories:
- name: How to use SwiftCurrent with SwiftUI
children:
- Getting Started with SwiftUI
- Working with NavigationView
- name: How to use SwiftCurrent with UIKit
children:
- Storyboards Intro
- Programmatic UIKit Intro
- name: Creating Workflows
children:
- FlowRepresentable
- PassthroughFlowRepresentable
- Workflow
- name: Creating Workflows in SwiftUI
children:
- View
- ViewControllerWrapper
- WorkflowItem
- WorkflowLauncher
- name: Creating Workflows in UIKit
Tyler-Keith-Thompson marked this conversation as resolved.
Show resolved Hide resolved
children:
- HostedWorkflowItem
- StoryboardLoadable
- UIViewController
- UIWorkflowItem
- name: Controlling Presentation
children:
- LaunchStyle
- FlowPersistence
- UIKitPresenter
- name: Type Erasure
children:
- AnyWorkflow
- AnyFlowRepresentable
- name: Underlying Types
children:
- LinkedList
- WorkflowError
- OrchestrationResponder
- FlowRepresentableMetadata
1 change: 1 addition & 0 deletions .github/abstract/How to use SwiftCurrent with SwiftUI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SwiftCurrent has first-class SwiftUI support. It’s designed to naturally feel like using any other SwiftUI tool. It is optimized to avoid common pitfalls. For example, SwiftCurrent does not use `AnyView` in its implementation. This means all of the animation and performance benefits you get when using SwiftUI normally you get when using SwiftCurrent.
Tyler-Keith-Thompson marked this conversation as resolved.
Show resolved Hide resolved
204 changes: 204 additions & 0 deletions .github/guides/Getting Started with SwiftUI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
## Overview

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:

![Preview image of app](https://raw.githubusercontent.com/wwt/SwiftCurrent/main/.github/wiki/swiftUI.gif)

## Adding the dependency

For instructions on SPM and CocoaPods, [check out our installation page.](https://github.com/wwt/SwiftCurrent/wiki/Installation#swift-package-manager)

## IMPORTANT NOTE

SwiftCurrent is so convenient that you may miss the couple lines that are calls to the library. To make it easier, we've marked our code snippets with `// SwiftCurrent` to highlight items that are coming from the library.

## Create your views

Create two views that implement `FlowRepresentable`.

```swift
import SwiftUI
import SwiftCurrent

struct FirstView: View, FlowRepresentable { // SwiftCurrent
typealias WorkflowOutput = String // SwiftCurrent
weak var _workflowPointer: AnyFlowRepresentable? // SwiftCurrent

@State private var email = ""
private let name: String

init(with name: String) { // SwiftCurrent
self.name = name
}

var body: some View {
VStack {
Text("Welcome \(name)!")
TextField("Enter email...", text: $email)
.textContentType(.emailAddress)
Button("Save") { proceedInWorkflow(email) }
Tyler-Keith-Thompson marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView(with: "Example Name")
}
}

struct SecondView: View, FlowRepresentable { // SwiftCurrent
typealias WorkflowOutput = String // SwiftCurrent
weak var _workflowPointer: AnyFlowRepresentable? // SwiftCurrent

private let email: String

init(with email: String) { // SwiftCurrent
self.email = email
}

var body: some View {
VStack {
Button("Finish") { proceedInWorkflow(email) }
Tyler-Keith-Thompson marked this conversation as resolved.
Show resolved Hide resolved
}
}

func shouldLoad() -> Bool { // SwiftCurrent
email.lowercased().contains("@wwt.com")
}
}

struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView(with: "[email protected]")
}
}
```

### Let's talk about what is going on with these views

#### **Why is `_workflowPointer` weak?**

<details>

The `FlowRepresentable` protocol requires there to be a `_workflowPointer` on your object, but protocols cannot enforce you to use `weak`. If you do not put `weak var _workflowPointer`, the `FlowRepresentable` will end up with a strong circular reference when placed in a `Workflow`.
</details>

#### **What's this `shouldLoad()`?**

<details>

It is part of the `FlowRepresentable` protocol. It has default implementations created for your convenience but is still implementable if you want to control when a `FlowRepresentable` should load in the workflow. It is called after `init` but before `body` in SwiftUI.
</details>

#### **Why is there a `WorkflowOutput` but no `WorkflowInput`?**

<details>

`WorkflowInput` is inferred from the initializer that you create. If you do not include an initializer, `WorkflowInput` will be `Never`; otherwise `WorkflowInput` will be the type supplied in the initializer. `WorkflowOutput` cannot be inferred to be anything other than `Never`. This means you must manually provide `WorkflowOutput` a type when you want to pass data forward.
</details>

## Launching the `Workflow`

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

```swift
import SwiftUI
import SwiftCurrent_SwiftUI

struct ContentView: View {
@State var workflowIsPresented = false
var body: some 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) }
Tyler-Keith-Thompson marked this conversation as resolved.
Show resolved Hide resolved
}.onFinish { passedArgs in // SwiftCurrent
workflowIsPresented = false
guard case .args(let emailAddress as String) = passedArgs else {
print("No email address supplied")
return
}
print(emailAddress)
}
}
}
}

struct Content_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
```

### Let's discuss what's going on here

#### **Wait, where is the `Workflow`?**

<details>

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

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

<details>

`WorkflowLauncher` is specialized with your `startingArgs` type. In `FlowRepresentable`, these types are supplied by the `WorkflowInput` and `WorkflowOutput` associated types. These all work together to create compile-time type safety when creating your flow. This means that you will get a build error if the output of `FirstView` does not match the input type of `SecondView`.
</details>

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

<details>

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

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

## Interoperability With UIKit
You can use your `UIViewController`s that are `FlowRepresentable` in your SwiftUI workflows. This is as seamless as it normally is to add to a workflow in SwiftUI. Start with your `UIViewController`

```swift
import UIKit
import SwiftCurrent
import SwiftCurrent_UIKit

// This is programmatic but could just as easily have been StoryboardLoadable
final class FirstViewController: UIWorkflowItem<Never, Never>, FlowRepresentable { // SwiftCurrent
typealias WorkflowOutput = String // SwiftCurrent
let nextButton = UIButton()

@objc private func nextPressed() {
proceedInWorkflow("string value") // SwiftCurrent
}

override func viewDidLoad() {
nextButton.setTitle("Next", 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
}
}
```

Now in SwiftUI simply reference that controller.

```swift
WorkflowLauncher(isLaunched: $workflowIsPresented) { // SwiftCurrent
thenProceed(with: FirstViewController.self) { // SwiftCurrent
thenProceed(with: SecondView.self) // SwiftCurrent
}
}
```
Loading