Better iOS Apps with View States
Last year I’ve written about using the State pattern to improve the architecture of the frontend slice of iOS apps. In the meantime, working with React, Redux and other approaches made my wonder: What oother techniques can be used to build better iOS apps?
The general idea behind React, a rendering system for single page applications (and, thanks to React Native, also apps) is to have composable components and something called unidirectional data flow. Basically, that just means that data only ever flows from the parent to the child components. Thanks to some magic React is doing, this is very efficient even if complex UIs are updated (by receiving a new state). Only the minimal changes necessary are applied to the DOM to transform the current to the desired state. That’s neat: It keeps the components dumb and stateless, handling complexity ‘somewhere else’.
In iOS, we usually have View Controllers that appear, display something and react to actions. As the name suggests, View Controllers control views, so their main purpose is to manage views, child view controllers and to handle actions that are created by user interactions, like a tap on a button. This management also includes processing data so views can actually work with the data that’s available – like formatting Strings or loading images.
Everything that is not part of the job description above should be handled somewhere else. Historically, not doing that has been the number one reason for bloated view controllers and, consequently, unmaintainable, untestable and messy iOS-projects.
Many approaches have emerged to make that problem somehow disappear. They universally share the trait that they all focus on distributing code into different places – View Models, Services, Routers, Presenters, Interactors, Managers and stuff like that. I’m not going to describe any of the existing approaches here, but you might want to read up on ReactiveSwift, VIPER, MVVM, and ReSwift to get some general idea.
Writing this post here indicates that I’m not satisfied with the solutions we have found over time and want to suggest a different way to decouple View Controllers from all the logic and magic that’s happening in the outside world – network access, asynchronous actions, offline states without keeping them from doing their main job: making sure that the right stuff is on display at all times.
Any solution to the problem states should adhere to some simple rules. First of all, the code we’re writing should be easy to test, both in unit and in integration settings. Secondly, the solution should adhere to the KISS-principle, meaning it should not introduce additional complexity but rather use simple constructs without relying on ‘magic constructs’, specific language features or generated code to work. Lastly, every component should have one clear responsibility and do only one thing, but that well. Let’s try to see if it works.
Our little demo app will be called Tracker. Tracker is just showing your current location on a map, with a button overlayed that lets you add your current position. The current position will then be added to the map. Quite simple. Let’s start by building this app in a ‘traditional’ approach, stuffing all the logic in the view controller. You can find all code in a Github Repo. The ‘traditional’ app is on the master branch, while the new approach can be found in the ‘new’ branch.
The app consists of exactly one View Controller, with all components (well, it’s just two) layed out in a Storyboard. It’s a MKMapView
and a regular UIButton
. I’ve also created a View Controller that simply displays an error string if the authorization status of the CLLocationManager
indicates that the app doesn’t have access to the Users’ location, with our main view controller adding that one in case the authorization status is problematic.
In the viewWillAppear
-Lifecycle method, the app queries the authorization status and, if necessary, requests authorization. We also implement the CLLocationManagerDelegate
-Protocol to handle any changes in that state, hiding or showing the authorization status.
To finally meet all requirements, the tap on the button is also handled and it adds the location as a MKPointAnnotation
to the map view. All well!
From a testing point of view, this thing is a nightmare and from a software design point of view, this setup violates the separation of concerns-paradigm wildly. It’s not exactly a View concern to make sure that the app is authorized to access the Users’ location. Or to keep and track the state of marked locations.
So, what we are going to do is refactor this thing to adhere a bit more to the idea of unidirectional data flow. The first step is to isolate all the data the view controller needs to properly display itself. Keep in mind that we only need to tell the View Controller what to display, not how to do it. For this, we’ll create a small struct that we’re going to call State
, and we’re going to use it to indicate what we’d like to have on the screen. For Tracker, the struct contains just two fields, and that would be trackedLocations
and authorized
, the first containing all the points the user has marked and the second whether location services can be used. It looks like that:
struct State {
var trackedLocations : [CLLocationCoordinate2D]
var authorized : Bool
}
Please prepend the State with the name of the current context. If you have a LoginViewController, call it LoginState and so on.
The next step is to make sure that our View Controller has just one method where it actually takes a State
-Struct as an input and updates its views so that they match the state that has been passed in. We’re going to take care of all the calls to the Location Services in a second. So here we go with our new method, which we’re going to call updateWithState:
func updateWithState(_ state: State) {
let annotations = state.trackedLocations.map { (coordinate) -> MKAnnotation in
let pointAnnotation = MKPointAnnotation()
pointAnnotation.coordinate = coordinate
return pointAnnotation
}
self.mapView.removeAnnotations(self.mapView.annotations)
self.mapView.addAnnotations(annotations)
self.errorViewController.view.isHidden = state.authorized
}
The only input is a state object, and the View Controller is completely resetting itself to display the data it just received. This is very important: the View Controller doesn’t keep any internal state, it just makes sure that whenever updateWithState
is called, the views match that state after the call.
But whos making that call? And who’s creating the state objects? This is something you might call ViewModel (without the binding magic) or simply Presenter. I’m going with Presenter here.
The presenter has a public API that exposes its functionality on a level that is independent from the presentation. That means that the method that makes sure that everything is prepared (Location Services) is not going to be named willAppear
but rather prepare
. The Presenter has no detail knowledge of the details of the View Controller and presentation. The other method we need as a means for interaction is ‘add current user location’, and we’re going to call that addCurrentLocation
. That’s the public API.
For testing purposes its far simpler to work with protocols than with classes, so we’re extracting those methods out into a Protocol. The final result, without the ‘actual code’, looks something like this.
protocol Presenter {
func prepare ()
func addCurrentLocation ()
}
class PresenterImplementation : Presenter {
func prepare () {
}
func addCurrentLocation () {
}
}
To make sure that neither the Presenter nor the View Controller rely on classes, but rather on protocols, we also extract the one method for updating the state on the View Controller out to a protocol, named StateUpdatable
. We’re going to keep an optional reference to a StateUpdatable
in the presenter. That’s the only way through which the Presenter is ever communicating with the View Controller.
So let’s move our logic to the Presenter. We still have to do the same stuff we did before: Ask for authorization when preparing, keep track of changes to the authorization state and make sure that we’re having a list of the tracked locations around.
func prepare () {
locationManager.delegate = self
if (CLLocationManager.authorizationStatus() == .notDetermined){
locationManager.requestWhenInUseAuthorization()
}
self.updateState()
}
So that’s not really much code. We make sure that we ask for authorizaton and we call updateState. Wait, what’s in there? It’s the method where the state is constructed and sent to the View Controller (or any other object that implements StateUpdatable).
private func updateState () {
var state = State()
state.authorized = CLLocationManager.authorizationStatus() == .authorizedWhenInUse
state.trackedLocations = trackedLocations
self.stateUpdatable?.updateWithState(state)
}
And this is really interesting. The only information the View Controller in our scenario needs is gathered with this few lines. We send it down and the VC processes it.
The next connection we need to establish is the one between the View Controller. We’re following the same scheme as before and add an Optional with the type of Presenter. That way we can easily use a Mock Object in Test scenarios in place of the real presenter, making testing quite easy.
var presenter : Presenter?
And we’ll add two calls to the presenter: One in the viewWillAppear
to the prepare-method, and one in the addButtonAction
-Method to the corresponding method on the presenter.
override func viewWillAppear(_ animated: Bool) {
self.presenter?.prepare()
Now for the wiring. We’ve built two classes that are losely coupled to one another, and now we need to make sure that both are actually aware that the other one exists. While I rely on Dependency Injection (Hey, Swinject!) for this in real projects, there’s not too much magic here and it can be simply done in a few lines of code. In this case we’re establishing the wiring in the viewDidLoad of the View Controller, but only if no presenter has been set yet. This makes sure that ones that are set (from a test, for example) won’t get overridden. This obviously violates the lose coupling component of this approach a bit, but it’s a tradeoff, and tradeoffs are sometimes ok.
So, this basically wraps up this little example. Let’s summarize the steps that we took to go from ‘classic’ to this new approach (View State is the working title, but there’s probably a better name for it).
First, we’ve extracted the View State to a separate struct and made sure that the View Controller is able to present itself by only relying on the values contained there. This is the whole React-idea ported to iOS: The only way data ever gets in is by setting this state. The View Controller becomes stateless and just makes sure that the Views it owns are properly updated to reflect the state it received.
We then moved the logic that is not strictly view-related to a Presenter. The Presenter is responsible for exposing all methods that are required for the use case to work on a semantical level. We’ve also created a protocol to make sure that we can easily use something else in place of the real presenter in a test scenario.
The final step was to make sure that the View Controller calls out to the Presenter whenever something happens that the presenter needs to handle – like preparing itself to be used or handling user actions.
The question remains: Is the extra effort required worth it? In fact, we added one class, the Presenter, one struct, the State and two protocols, adding complexity to the code base. But on the other hand we’ve decoupled the UI from any domain logic, created a generic presenter that can also be used from other View Controllers and increased the testability of our code base.
Having a single method that handles all state changes in the View Controller might seem a bit odd at first, things like animating changes in Table Views or redundant calls come to mind. But during development, this proved to make life much, much easier: Just place a breakpoint there and see whether the data is the problem or the logic in the View Controller itself. For animating changes in Table Views or Collection Views, I suggest looking at Diff.swift or friends. While using a library like this requires the View Controller to keep the previous state around, doing so is necessary in scenarios where Table Views are used anyway (and it doesn’t violate the principle of the View Controller not keeping state, since the state is strictly kept for managing its views.)
And does this really adhere to principles laid out in the beginning? Let’s see:
- Doesn’t use voodoo magic: All constructs used are easy to understand, even by rather inexperienced developers, thus adhering to the KISS-principle.
- Everything can be built using only Xcode, no third party libraries required.
- The Presenter and View Controller can be tested independently from one another.
- Separation of concerns: Both the Presenter and the View Controller have a clear cut set of responsibilities and both do nothing more.
Also, one aspect about this whole approach I came to appreciate is the fact that it’s quite easy to start using it, as it can be used in isolation on single components and usage can be gradually extended, if so desired.
This whole idea is clearly inspired by Redux (States), React, ReSwift and a lot of great discussions with collegues and friends. Thank you!