FlowState
is a lightweight framework used for creating swifty, composable Flow Coordinators.
In the MVC pattern a ViewController's job is to react to user interaction to display and gather content from the user.
Often a Flow
is a string of ViewControllers that define a feature of an app.
All too often the logic that defines how information is moved from one view controller to the next,
and even the logic that moves the flow from one view controller to the next, ends up sandwiched
into the viewcontrollers themselves.
This makes for messy hard to read code that is hard to refactor and even harder to reorder.
Flow Coordinators are the answer to this problem. A Flow Coordinator
manages the flow of information - and the flow of View Controllers -
across several pages in a flow. The ViewControllers are written to be as simple and clean as possible. Mainly a view controller will only display Content
and collect Result
.
FlowState
provides an easy to compose and easy to ready way to create view controlller flows in swift!
The goal is to write a flow of view controllers and handle the comlex data piping in an easy way.
A FlowState
coordinator is made up of a string of FlowStep
. Each step manages a single part of a flow.
When a FlowStep
starts, the flow is paused until the step completes. Once compete, the next step begins.
FlowStep
makes managing the flow of information and content a breeze.
A FlowStep
is given a FlowStepIntent
, which is usually owned by a view controller.
The FlowStepIntent
is how the step manages its content, and also tells the step when it has completed.
Lets create a simple flow for collecting some information from a user profile. First, lets display what information we have by displaying the profile of the user.
A flow step has two main parts, Content
which defines the content that the step displays and
Result
which defines the information that the step collects. Some steps don't collect information,
but instead an interaciton.
This first step will display a user profile view controller. The user profile view controller displays
information from a UserProfile
struct. The profile view controller wants to refresh its contents
by making a network call to download the profile data when it is displayed. Additionally the
profile view controller has a button that allows the user to update their name.
The profile view controller then has two result actions, download and updateName. Each defined in
an enum UserProfileAction
.
The profile view controller also has a FlowStepIntent
that it owns called results
.
The profile view controller uses the results to get its contents and also tells the intent of any user interaction.
/// Create a FlowStep that displays the profile view controller.
let displayStep = FlowStep(UserProfile.self, UserProfileAction.self, identifier: "Profile", { (step) in
/// This block is called each time the step begins.
/// Make a new Profile Display VC
let vc = UserProfileViewController()
/// Set the vc on the navigation controller step.
navigationController.setViewControllers([vc], animated: false)
/// Return the View Controller's FlowStepIntent `results`.
return vc.results
})
The displayStep has two outcomes, one will show a view controller that will edit the user name, the other will download the new profile.
Let's create a step for downloading the new profile. This step does not have any content, but the result is
a new UserProfile
object.
/// Create a step that downloads the user profile from the internet
let networkStep = FlowStep(Any.self, UserProfile.self, identifier: "User Profile Network", { (step) in
/// Show a loader View Controller
let loader = LoadingModalViewController()
navigationController.present(loader, animated: true, completion: nil)
/// Create step results for this step.
let stepResults = FlowStepIntent(Any.self, UserProfile.self)
/// Make an async network call.
UserProfile.getUser() { (newProfile) in
/// After the download has completed, call completion with the results.
stepResults.complete(newProfile)
}
return stepResults
}) { [unowned displayStep] (step, newProfile) in
if let newProfile = newProfile {
/// Set the new profilew on the display step, which automatically updates it's view controller.
displayStep.content = newProfile
}
/// Dismiss the loading modal.
navigationController.dismiss(animated: true, completion: nil)
}
Now lets create a step that allows the user to edit their name.
The EditNameViewController has a content type of a String
and a result type of a new String
let editNameStep = FlowStep(String.self, String.self, identifier: "Edit Name", { (step) in
let vc = StringEntryViewController()
navigationController.pushViewController(vc, animated: true)
return vc.results
})
Next lets create a network call that updates the username and returns the updates User Profile:
/// Create a step that updates the user profile
let updateNameStep = FlowStep(String.self, UserProfile.self, identifier: "Update Profile Step", { (step) in
/// Show a loader View Controller
let loader = LoadingModalViewController()
navigationController.present(loader, animated: true, completion: nil)
/// Create step results for this step.
let stepResults = FlowStepIntent(Any.self, UserProfile.self)
/// Make an async network call that updates the user profile with the step's content.
UserProfile.updateName(step.content) { (newProfile) in
/// After the download has completed, call completion with the results.
stepResults.complete(newProfile)
}
return stepResults
}) { [unowned displayStep] (step, newProfile) in
if let newProfile = newProfile {
/// Set the new profilew on the display step, which automatically updates it's view controller.
displayStep.content = newProfile
}
/// Dismiss the loading modal.
navigationController.dismiss(animated: true, completion: nil)
/// Optionally you could pop the nav controller here to go back to the profile.
}
Now lets link the editNameStep
to the updateNameStep
First we need to add a completion closure to the editNameStep
which forwards it's results to the updateNameStep
.
Then we set the next step of editNameStep
to be the updateNameStep
.
editNameStep.setCompletion { (step, newName) in
guard let newName = newName else {
step.nextStep = nil
return
}
/// Forward the results of this step to the updateNameStep
updateNameStep.content = newName
step.nextStep = updateNameStep
}
Finally, now that we have all of our steps created lets set the branching logic on the step that manages the UserProfileView
.
/// Set the display step completion block to switch between its results
displayStep.setCompletion { (step, action) in
guard let action = action else { return }
switch action {
case .editProfile:
/// Edit profile was pressed.
editTextStep.content = step.content
step.nextStep = editTextStep
case .downloadProfile:
/// Download profile was pressed.
step.nextStep = fakeNetworkStep
}
}
And thats it! You now have a composable flow. If you wanted to reorder the views, or make changes you can easily connect the flow steps in a different order without worrying about jumping into view controller code.
Need more?
To run the example project, clone the repo, and run pod install
from the Example directory first.
FlowState is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'FlowState'
buba447, buba447@gmail.com
FlowState is available under the MIT license. See the LICENSE file for more info.