Implementation of a Flow on iOS
Examples of implementation and composition of Flows
This article builds on information covered in the first writing of this series.
After having had a look at the main building blocks of a Flow, we can now see how we can implement them. All the examples shown here are built around fictional view controllers: their only requirement is to have a way to communicate back the result that they can produce. In a real world scenario, these results will be much more complex components: the data they produce could be a complex object, a DTO, or whatever you want. There is no limitation.
Also, I’m used to have a Flow create a VIPER module (maybe through an external factory, which contains and links together all the dependencies): it will get a reference to the view controller to present; the Flow will also be set as the Router. In fact, in practical scenarios, the Flow most of the times implements the Router interface of different VIPER modules, one for each module it needs to present.
If you use different design patterns, that is fine. Your logic to tie together all the components will eventually be slightly different, but what won’t change is how to operate a Flow component.
Simple Flow with a single VC to present
Let’s start from the a very basic example. We have a ButtonViewController (code not show here, but can be found in the GitHub project) which is simply showing a button. This view controller has a delegate property which is called when the button is pressed.
The example is really simple, but is all we need to show how to launch the Flow, how to handle the responses from any view controller that we presented, and finally how to pass back the flow result to the object which started it.
- (1) we defined the result type for our Flow to be SimpleModalFlowResult, an enum with a single case (for what we want to demonstrate, this is enough)
- (2) we have a factory, through which obtain a ViewControllerPresenter
- (3) after creating the view controller that we want to present and setting the delegate, we use the ViewControllerPresenter to let it present the view controller.
- (4) we get back a ViewControllerDismisser, which we keep in a private var
- (5) on the completion we also invoke the flowStarted() function on the FlowHandler
- (6) the terminate() function is dismissing the view controller presented in a non-animated way. This may be required sometimes when we want to terminate the Flow prematurely from the outside, without waiting for it to finish
- (7) once the delegate function is triggered, we invoke the flowFinished(result:dismisser:) function on the FlowHandler, passing the final flow result along with the ViewControllerDismisser responsible to unwind the UI state
To launch this Flow, all we need to do is:
Here we are also using the FlowController to automatically manage the Flow, so we don’t have to manually keep a strong reference to it while it’s being presented. This is not strictly necessary in this example, but it just shows how we can use a FlowController even outside of a Flow (and not only for sub-Flows).
Flow with different VCs presented + a sub-Flow
Let’s see a more complex scenario, where a Flow has to present more than a single view controller. I’ll omit a lot of code not strictly relevant to understand how the overall process works (could find the complete version in the GitHub project).
The start() function is doing is pretty much the same as the previous example: presenting a view controller and setting the delegate. (1)
What changes are the actions we perform when the callback is called. (2)
We launch a new Flow (through the FlowController, which takes care of holding a strong reference until it has finished). (3)
On the onCompleted callback we switch over the result of the sub-Flow. (4)
In case of .error, we let the main Flow terminates with a .canceled(reason:) result. (5)
In case of .success , we present another view controller (6). When it will call the confirmResult() callback (7), we will dismiss it (8), and on the completion we will let the main Flow finishes with a .completed(success: true) result. This is done by calling the flowFinished(result:dismisser:) function on the FlowHandler (9).
Let’s take a look at a part of the code inside the sub-Flow that we are here launching:
This shows one of the places where this SimpleNavigationFlow may terminates. It’s important to see that we always inform the caller through the FlowHandler functions: in this case using the flowFinished(result:dismisser).
From this Flow’s point of view, in fact, we have no idea if it is being launched as a normal Flow (ie. from a view controller) or as a sub-Flow (what is happening in our specific case).
This example may look complex at first, but this is exactly what I wanted to show: how to handle different kind of tasks and how to combine them, depending on our needs:
- present a single view controller
- launch a new sub-Flow
- either terminates the main Flow, or present another view controller
- terminates the main Flow
All this without knowing at all what the sub-Flow is doing internally, but simply knowing its FlowResultType.
Build your Flow
With the examples shown above, it should now be easier to know how to create a Flow. The main things to remember about it are:
- use ViewControllerPresenter when presenting a view controller. Keep the reference of the ViewControllerDismisser to be able to later dismiss it.
- communicate back to the caller through the FlowHandler, by invoking its flowStarted() and flowFinished(result:dismisser:) functions. The ViewControllerDismisser to pass here is the one which unwind what the Flow presented. In most of the cases it will be the one related to the first view controller that we presented, but it may be not. Up to you.
- do not unwind the UI from within the Flow before calling flowFinished(result:dismisser:) : it should be responsibility of the caller to handle this. This will make the same Flow be reusable in different places, with different requirements (ie. dismiss the Flow vs. do not dismiss the Flow but leave it on the stack, or dismiss it animated vs dismiss it not animated).
Real-world case: TransferWise
We are starting to use this approach in many new components that we are building. The Profile Flows (both the Personal and the Business one) are built upon this idea, for example.
The Business Flow is even launching the Personal Flow as a sub-Flow. It is also launching other sub-Flows along with standalone view controllers. It is presented in 5 different places throughout the app: all of them need to be handled differently (completely dismiss the flow when finished, either animated or not; or leave it on the navigation stack so the user can eventually come back; or perform some extra custom actions once finished). All of this has been easily achieved because of the properties of the Flow components.
The Onboarding Flow (shown when a new user create a new account) is composed, among the others, to launch the Business Flow as a sub-Flow. To make this happens, not a single line of code has been changed in the Business Flow, because of the modularity and how the components stick together.
We have seen how creating some Flows by putting together the different building blocks. We have seen a simple example, where only a view controller is being presented. We have also seen a more complex case, which handles multiple view controllers and a sub-Flow as well. With these examples, we have shown basically all you need to start create any Flow that you may need. The concepts shown here, in fact, could be reused on a much larger/different Flow that you may need: the core ideas and interactions between the parts are still the same.
In the next and final part we’ll take a look at how to write unit tests for the Flow components that we just created.