Our Experience Architecting iOS Apps with Viper
With the benefits of a small development team, products can be prototyped and tested with users quickly and in a cost-effective manner. If customer testing is successful, requirements expand, additional developers are added, and the product evolves through multiple versions of increasing complexity and scope.
This is where developers often encounter growing pains. Software rapidly developed to yield quick results but with less-than-industrial-strength scaffolding becomes difficult to manage as additional developers are added to the team (often producing what developers pejoratively call massive view controllers). Crossing the chasm to a mid to large development team, therefore, creates a need for thoughtful preemptive design of software architecture.
We were presented with this challenge at Location Labs for our Locator iOS application. Locator was initially developed by one developer and quickly grew to a team of 6-8 as customer adoption increased and the product matured. Recognizing the need for a redesign of our software, we rebuilt and rearchitected Locator from the ground up, with the following criteria in mind:
- We wanted our codebase to be highly modular, allowing multiple developers to contribute code without interfering with each other.
- We wanted our architecture to scale well as new features and requirements were added.
- We wanted to enable quick and easy visual and backend configurations.
- We wanted our app to be thoroughly and easily testable.
- We wanted new developers to be able to learn the architecture quickly.
What we came up with:
We came up with an architecture based on Viper, with a few modifications. Viper is an architecture framework designed with the goal of decomposing application logic into distinct layers, each owning a single responsibiliy. This solves the often-encountered problem in software of ambiguity–where should code reside, and what responsibilities, or set of concerns, does this code have? The problem of ambiguity is exacerbated in mid to large developer teams, as each developer may develop his or her own solution to the same problems, resulting in inconsistencies, redundancies, complex dependencies and conflicts. By clearly separating responsibilities into modular units of code, Viper aims to structure code in a way that is highly scalable and maintainable, with minimal decay in quality as features are redesigned, refactored, or new features are added.
The separation of concerns are made into the following five parts, which make up the Viper acronym:
- View: displays what it is told to by the Presenter and relays user input back to the Presenter.
- Interactor: contains the business logic as specified by a use case.
- Presenter: contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).
- Entity: contains basic model objects used by the Interactor.
- Routing: contains navigation logic for describing which screens are shown in which orders.
These come together to constitute a Viper "module". Modules correlate with screens in an iOS application - e.g. SignUpModule, MainScreenModule, SettingsModule.
For more information on, the reader is encouraged to visit the following sites:
Our Implementation of Viper
In addition to the traditional Viper stack, we broke down responsibilities further to include a Data Manager, a Styler, an Assembler, and a Feature:
- The job of the Data Manager is to know which services the Interactor needs to connect with to perform its functions. This allows us to keep the Interactor focused solely on business logic, without having to know which services it needs to connect with.
- The Style class encapsulates all visual styling-related code (colors, fonts, dimensions). Decoupling style-related code from the View Controller provides us with the flexibility to override styles, inherit common styles, and swap styles for alternative ones. This is very useful to us as it enables us to allows external business partners to easily customize the look and feel of the application to their liking.
- The Assembler is responsible for encapsulating the instantiation details of Viper modules and "wiring up" their dependencies.
- The Feature provides a readable reference by which Viper modules are loaded into memory. The Feature also enables Viper modules to be loaded on an as-needed basis, instead of loaded all at once.
Here is an example flow of execution for Sign Up:
Isn't it a pain to create Viper modules for each screen?
By leveraging Xcode templates, scaffolding can be created instantly. All that you need to do is go to File -> New -> File and select Viper Module.
How do Viper modules tie in to each other?
Our Viper Config class bootstraps Viper modules and component frameworks on application launch. Bootstrapping involves two steps:
1. Configure Viper with assemblies for component frameworks and environment properties as Json files:
2. Register Viper modules by their Feature:
All that is needed after App is configured is to route to the first Viper module like so:
We have created a basic Weather application in Viper to demonstrate this further. Please see our Github project.
What we learned
On first impression, Viper may appear unnecessarily convoluted, especially applied to simple screens. Scaffolding overhead for simple screens is admittedly a tradeoff. In our experience, however, this is well counterbalanced by the benefits of Viper's principled breakdown of application logic into distinct layers of responsibilities. Viper essentially puts iOS coding "on rails", guiding developers in writing clear, consistent and modular code, and minimizing ambiguity of where code should reside. We have since adopted Viper for all of our major products at Location Labs, and this has afforded us two additional benefits: 1) Allowing developers to flexibly change teams to meet fluctuating resourcing needs without having to learn a new architecture, and 2) Increasing code sharing in general, as Viper components can be easily shared across product teams.