Building Screen Flows with VIPER on iOS
We use the VIPER architecture pattern in the Brigade iOS app. We’ve had to build a few screen flows with VIPER so I thought I’d share some of our learnings.
We haven’t found a one-size-fits-all design pattern so we’ve identified three: Single-Module Flows, Multi-Module Flows, and Linked-List Flows.
Single-Module Flows
In this pattern, the whole flow is contained in a single VIPER module. Each step is a separate view controller. There’s a single presenter whose user interface is dynamically set to the current view controller.
Where and why we use single-module flows
We use this pattern in the voter verification flow because we don’t have to reuse views outside of the flow. If you don’t need to use existing modules inside your flow then the Single-Module pattern is pretty convenient and practical.
How it works
The wireframe is the orchestrator. It initializes the view controllers and pushes them onto the navigation controller as needed.
When a view controller reports that it’s “done”, it notifies the wireframe. At this point the wireframe pushes the next view controller onto the stack and the process continues until the last step.
Pros
- Minimum overhead and VIPER boilerplate. There’s no need to create an entire module for each step in the flow.
- Simple and easy to manage. No need to jump around or context switch to different modules.
Cons
- Bloated application logic stack. The presenter, interactor, and data manager handle all the business logic for the entire flow so this can start to get heavy depending on the flow.
Multi-Module Flows
Unlike the Single-Module pattern, in this pattern the flow is composed of many VIPER modules (one per step). There’s also a coordinator module responsible for orchestrating the flow.
Where and why we use multi-module flows
We use this pattern in the onboarding flow because it requires a little more flexibility from a product standpoint. For example, we had to include our existing inviter into this flow.
How it works
The coordinator wireframe’s job is to initialize everything and manage the steps in the flow. It does this by using a finite state machine.
When a step module tells the coordinator “Hey, I’m done!”, the coordinator grabs the next module’s view controller and pushes it onto the navigation controller.
Pros
- Ability to inject existing modules into the flow.
- Single Responsibility Principle / Separation of concerns. The coordinator doesn’t need to know about the details of each step.
- Managing the step order is easy. There’s only one file to check.
Cons
- Adding new steps is costly. You have to create a new VIPER module for each one.
- The coordinator wireframe implements a lot of protocols.
Linked-List Flows
This last pattern is the most “compact” in terms of code. The “flow” here is implicit and is controlled by the modules in the flow directly. When module 1 is done, it just routes onto module 2, etc..
Where and why we use linked-list flows
We use this pattern in the flow where after signing a petition we ask the user to invite others to sign. This flow only had two steps so the Linked-List flow pattern was most applicable.
How it works
When one module is done, its wireframe initializes and routes to the next module. This routing happens either directly or using a central router (depending on how your app is architected overall).
Pros
- No need for a coordinator module. The flow is implicitly controlled by each module.
Cons
- Modules in the flow have to be dedicated to the flow. If a module needs to work both inside and outside a flow then it must be configurable as such, perhaps via a state flag during initialization, which is probably not the best pattern.
- Changing the order of the flow can be difficult.
- Reasoning about the flow is challenging. There is no birds eye view of the steps in the flow. Instead, you have to follow the breadcrumbs from module to module.
Final Thoughts
There is no perfect solution for managing and orchestrating flows with VIPER. While each pattern has its strengths and weaknesses, I prefer the Single-Module flow due to its flexibility. Of course, the type of flow you choose will depend on your feature requirements.
In general I find that short and simple flows make for better user experiences, simpler code, and preserved sanity.