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 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:
Adding the Dependency
For instructions using Swift Package Manager (SPM) and CocoaPods, check out our installation page. This guide assumes you use SPM.
IMPORTANT NOTE
SwiftCurrent is so convenient that you may miss the couple of 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
.
First view:
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) } // SwiftCurrent
}
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView(with: "Example Name")
}
}
Second view:
import SwiftUI
import SwiftCurrent
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) } // SwiftCurrent
}
}
func shouldLoad() -> Bool { // SwiftCurrent
email.lowercased().contains("@wwt.com")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView(with: "Example.Name@wwt.com")
}
}
What Is Going on With These Views?
Why is _workflowPointer
weak?
FlowRepresentable._workflowPointer
is required to conform to the
FlowRepresentable
protocol, 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
.
What’s this shouldLoad()
?
FlowRepresentable.shouldLoad()
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.
Why is there a WorkflowOutput
but no WorkflowInput
?
FlowRepresentable.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.
FlowRepresentable.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.
Launching the Workflow
Next we add a WorkflowView
to the body of our starting app view, in this case ContentView
.
import SwiftUI
import SwiftCurrent_SwiftUI
struct ContentView: View {
@State var workflowIsPresented = false
var body: some View {
if !workflowIsPresented {
Button("Present") { workflowIsPresented = true }
} else {
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 {
print("No email address supplied")
return
}
print(emailAddress)
}
}
}
}
struct Content_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What’s Going on Here?
Wait, where is the Workflow
?
Workflow
type is handled by the library when you start with a
WorkflowView
.
Where is the type safety I heard about?
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 launchingWith
and passedArgs
?
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.
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
.
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.
WorkflowView(isLaunched: $workflowIsPresented) { // SwiftCurrent
WorkflowItem(FirstViewController.self) // SwiftCurrent
WorkflowItem(SecondView.self) // SwiftCurrent
}