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 UIKitExample scheme in SwiftCurrent.xcworkspace.
The app in this guide is going to be very simple. It consists of a screen that will launch the Workflow, a screen to enter an email address, and an optional screen 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 on SPM and CocoaPods, check out our installation page..
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 the Convenience Protocols for Storyboard Loading
It is best practice to use the StoryboardLoadable protocol to connect your FlowRepresentable to your storyboard. Additionally, to limit the amount of duplicate code, you can make a convenience protocol for each storyboard.
import UIKit
import SwiftCurrent_UIKit
extension StoryboardLoadable { // SwiftCurrent
// Assumes that your storyboardId will be the same as your UIViewController class name
static var storyboardId: String { String(describing: Self.self) }
}
protocol MainStoryboardLoadable: StoryboardLoadable { }
extension MainStoryboardLoadable {
static var storyboard: UIStoryboard { UIStoryboard(name: "Main", bundle: Bundle(for: Self.self)) }
}
NOTE:
StoryboardLoadableis only available in iOS 13.0 and later.
Create Your View Controllers
Create two view controllers that both conform to MainStoryboardLoadable and inherit from UIWorkflowItem.
First view controller:
import UIKit
import SwiftCurrent_UIKit
class FirstViewController: UIWorkflowItem<String, String>, MainStoryboardLoadable { // SwiftCurrent
private let name: String
@IBOutlet private weak var emailTextField: UITextField!
@IBOutlet private weak var welcomeLabel: UILabel! {
willSet(this) {
this.text = "Welcome \(name)!"
}
}
required init?(coder: NSCoder, with name: String) { // SwiftCurrent
self.name = name
super.init(coder: coder)
}
required init?(coder: NSCoder) { nil }
@IBAction private func savePressed(_ sender: Any) {
proceedInWorkflow(emailTextField.text ?? "") // SwiftCurrent
}
}
Second view controller:
import UIKit
import SwiftCurrent_UIKit
// This screen shows an employee only screen
class SecondViewController: UIWorkflowItem<String, String>, MainStoryboardLoadable { // SwiftCurrent
private let email: String
required init?(coder: NSCoder, with email: String) { // SwiftCurrent
self.email = email
super.init(coder: coder)
}
required init?(coder: NSCoder) { nil }
@IBAction private func finishPressed(_ sender: Any) {
proceedInWorkflow(email) // SwiftCurrent
}
func shouldLoad() -> Bool { // SwiftCurrent
return email.contains("@wwt.com")
}
}
What Is Going on With These View Controllers?
Where are the FlowRepresentables you mentioned earlier?
class FirstViewController: UIWorkflowItem
, FlowRepresentable, MainStoryboardLoadable
, but the
FlowRepresentable is not specifically needed, so we excluded it from our example.
Why is FlowRepresentable not needed in the declaration?
FlowRepresentable by the combination of
UIWorkflowItem and
StoryboardLoadable
- The
UIWorkflowItemclass implements a subset of the requirements forFlowRepresentable. .
-
StoryboardLoadableimplements the remaining subset and requires that it is only applied to aFlowRepresentable.
Why these initializers?
StoryboardLoadable helps guide XCode to give you compiler errors with the appropriate fix-its to generate
required init?(coder: NSCoder, with args: String). These initializers allow you to load from a storyboard while also having compile-time safety in your properties. You will notice that both view controllers store the argument string on a
private let property.
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
viewDidLoad().
Launching the Workflow
Next, we create a Workflow that is initialized with our FlowRepresentables and launch it from a view controller that is already loaded onto the screen (in our case, the ‘ViewController’ class provided by Xcode).
import UIKit
import SwiftCurrent
import SwiftCurrent_UIKit
class ViewController: UIViewController {
@IBAction private func launchWorkflow() {
let workflow = Workflow(FirstViewController.self) // SwiftCurrent
.thenProceed(with: SecondViewController.self) // SwiftCurrent
launchInto(workflow, args: "Some Name") { passedArgs in // SwiftCurrent
workflow.abandon()
guard case .args(let emailAddress as String) = passedArgs else {
print("No email address supplied")
return
}
print(emailAddress)
}
}
}
What Is Going on Here?
Where is the type safety I heard about?
Workflow has compile-time type safety on the input/output types of the supplied
FlowRepresentables. This means that you will get a build error if the output of
FirstViewController does not match the input type of
SecondViewController.
What’s going on with this passedArgs?
onFinish closure for
UIViewController.launchInto(_:args:withLaunchStyle:onFinish:) provides the last passed
AnyWorkflow.PassedArgs in the workflow. For this workflow, that could be the output of
FirstViewController or
SecondViewController depending on the email signature typed in
FirstViewController. To extract the value, we unwrap the variable within the case of
.args() as we expect this workflow to return some argument.
Why call abandon()?
Workflow.abandon() closes all the views launched as part of the workflow, leaving you back on
ViewController.
Testing
Installing Test Dependencies
For our test example, we are using a library called UIUTest. It is optional for testing SwiftCurrent, but in order for the example to be copyable, you will need to add the UIUTest Swift Package to your test target.
Creating Tests
import XCTest
import UIUTest
import SwiftCurrent
// This assumes your project was called GettingStarted.
@testable import GettingStarted
class SecondViewControllerTests: XCTestCase {
func testSecondViewControllerDoesNotLoadWhenInputIsEmpty() {
let ref = AnyFlowRepresentable(SecondViewController.self, args: .args(""))
let testViewController = (ref.underlyingInstance as! SecondViewController)
XCTAssertFalse(testViewController.shouldLoad(), "SecondViewController should not load")
}
func testSecondViewControllerLoadsWhenInputIsContainsWWTEmail() {
let ref = AnyFlowRepresentable(SecondViewController.self, args: .args("Awesome.Possum@wwt.com"))
let testViewController = (ref.underlyingInstance as! SecondViewController)
XCTAssert(testViewController.shouldLoad(), "SecondViewController should load")
}
func testProceedPassesThroughInput() {
// Arrange
var proceedInWorkflowCalled = false
let expectedString = "Awesome.Possum@wwt.com"
let ref = AnyFlowRepresentable(SecondViewController.self, args: .args(expectedString))
var testViewController = (ref.underlyingInstance as! SecondViewController)
// Mimicking the lifecycle of the view controller
_ = testViewController.shouldLoad()
testViewController.loadForTesting() // UIUTest helper
testViewController.proceedInWorkflowStorage = { passedArgs in
proceedInWorkflowCalled = true
XCTAssertEqual(passedArgs.extractArgs(defaultValue: "defaultValue used") as? String, expectedString)
}
// Act
(testViewController.view.viewWithAccessibilityIdentifier("finish") as? UIButton)?.simulateTouch() // UIUTest helper
// Assert
XCTAssert(proceedInWorkflowCalled, "proceedInWorkflow should be called")
}
}
While this team finds that testing our view controllers with UIUTest allows us to decrease the visibility of our properties and provide better coverage, UIUTest is not needed for testing SwiftCurrent. If you do not want to take the dependency, you will have to elevate visibility or find a way to invoke the finishPressed method.
What is going on with testSecondViewControllerDoesNotLoadWhenInputIsEmpty?
shouldLoad to validate if the provided input gets us the results we want.
What is going on with testProceedPassesThroughInput?
proceedInWorkflow closure so that we can confirm it was called. Finally, we invoke the method that will call proceed. The assert is verifying that the output is the same as the input, as this view controller is passing it through.
I added UIUTest, why isn’t it hitting the finish button?
loadForTesting(), your view controller doesn’t make it to the window and the hit testing of
simulateTouch() will also fail. Finally, make sure the button is visible and tappable on the simulator you are using.
Interoperability With SwiftUI
You can use your SwiftUI Views that are FlowRepresentable in your UIKit workflows. Start with your View.
import SwiftUI
import SwiftCurrent
struct SwiftUIView: View, FlowRepresentable { // SwiftCurrent
weak var _workflowPointer: AnyFlowRepresentable? // SwiftCurrent
var body: some View {
Text("FR2")
}
}
Now in your UIKit workflow, simply use a HostedWorkflowItem.
launchInto(Workflow(HostedWorkflowItem<SwiftUIView>.self)) // SwiftCurrent
View on GitHub
Using Storyboards Reference