As things grow quickly there are often strange, troublesome side-effects. This is true for teenagers, and it is also true for apps.
Last year our Bipsync Notes app grew very quickly. From our initial 1.0 release through to the latest, 1.7.2, we added several features that expanded the codebase from 10,036 lines of code to 31,571 – an increase of 214%.
Most of those new lines of code were directly related to new features as opposed to the application framework that supports them, and since the first version of the app was very much an example of a Minimum Viable Product, that meant the architecture of the app was long overdue some love and attention.
We knew what needed improving: on startup the app barrelled headlong through its setup routines before slapping a view on the screen, and the logic to achieve this was convoluted and constrained to a handful of classes that were doing too much.
We knew what needed to be done: find a way to make the app modular, which would then allow us to separate the ‘backend’ logic – setting up the database, loading credentials from the keychain, and so on – from the ‘frontend’ logic of the application user interface. With the logic broken down in this way we’d be able to be much more intelligent about the app’s workflow and, crucially, we’d be able to improve our unit test coverage too.
Developing an architecture
Apple are particularly good at providing sample code to illlustrate how to work with their APIs, but as others have noted they very rarely comment on how applications should be structured, instead leaving that decision to the developer. Since apps can vary wildly in nature this makes sense, but it’d be nice to at least have some guidance as to what Apple has tried in the past, and the approaches they use in their apps today.
Each iOS app includes a class that implements the UIApplicationDelegate protocol; this class is effectively the entry point into the system. Some simple apps may use storyboards to set up the initial views and have very little in the way of contributing classes – in these cases the delegate will be svelte.
Now our app isn’t the most complicated app in the Store but nevertheless it does quite a lot on startup. We set up the Core Data stack, our full-text-search index, process any pending background fetches, sync app config from the server, set up a web server to run the note editor and more. So following the approach you’ll see in lots of sample code – Apple’s own included – of orchestrating all this from within the delegate was clearly no longer feasible. A different approach was needed.
Picking a pattern
Numerous patterns exist which could be used to improve iOS application architecture. A glance through issue thirteen of objc.io magazine reveals two of the most famous – MVVM, or “Model-View-ViewModel”, and VIPER (“View-Interactor-Presenter-Entity-Routing”). However both these and other similar approaches are concerned with reducing the “Massive View Controller” problem, not the “Massive App Delegate” one we were faced with.
We’ve strived to compose our view controllers of smaller, dedicated classes even since the early days of the app, and with storyboards decoupling the views from their controllers we felt that aspect of the app to be sound for now.
What we needed was an approach that facilitated logic at a level above view controllers, but allowed us to continue with our dependency-injected objects and storyboarded navigation.
This approach would, for example, allow us to do something like transition to a progress view while the database initializes – this can often take a few seconds to finish so we do it in a background thread to keep the UI smooth – before transitioning on to the app’s “home” view once the database and any additional dependencies are ready.
We wanted to eliminate any question of state in this process, as this had become the main source of complexity in our existing app delegate. We were seeing repeated logical questions like “Is the user logged in?” or “Is the database ready?” and so on, right through the code.
In fortuitous fashion this blog post by Soroush Khanlou popped up in my RSS reader while I was pondering how best to approach our problem. He introduces the concept of coordinators, a coordinator being “an object that bosses one or more view controllers around”. I urge you to read the post to understand the approach fully, but in a nutshell:
- A root coordinator, called the app coordinator, is created and retained by the app delegate.
- The app coordinator contains child coordinators which break the app’s logic into modular chunks, e.g. Authentication, Settings.
- A coordinator is responsible for deciding the interface to present to the user, and interacting with the view controllers in that interface to navigate the flow of the app
By employing this pattern two of the biggest causes for bloat in our app delegate – component management and UI management – were immediately relocated to smaller, specialised classes. An example is in order.
Beforehand, our app delegate would have been doing something like this:
Which is a slimmed down extract from our actual class. The problems with this approach include:
- The app delegate is doing way too much. It receives calls from the OS, sets up and stores dependencies, queries state, configures the UI and more. It should be a conduit to the OS and nothing else.
- Dependencies are set up even though we may not need them. For example the authentication flow has no need for the HTTP server, but the bootstrapping code sets it up anyway because the home view will eventually need it. Sure we could have a second bootstrapping method and call it later, but that doesn’t make the code any easier to understand.
- Communication via notifications allows the delegate to respond to changes in state by updating the UI, but what if other objects also observe those notifications and the delegate’s actions compromise the actions of others? The use of notifications means the delegate isn’t coupled to UI objects (a good thing) but it makes the app as a whole completely indeterminate (a bad thing).
- The three above problems all become bigger problems as the app grows, preventing it from scaling effectively.
After refactoring the app delegate to use a coordinator, it effectively looks like this:
Our implementation differs from that in Soroush’s post because we don’t base the entire app off a root view controller – so we instead pass in the window, and let child coordinators of the AppCoordinator set the window’s root view controller as appropriate. The general approach is the same as Soroush’s though, so please refer to that for specifics. All the bootstrapping logic has been moved into the coordinator chain. The AppCoordinator creates general dependencies like a NSURLSession object, but no more – each coordinator is then responsible for instantiating its own dependencies.
Speaking of child coordinators – we have a few:
- AuthenticationCoordinator: if the user is not deemed to have a valid session we start an “authentication coordinator”, which presents a login view and manages communication with the server to validate credentials.
- DatabaseCoordinator: after authentication we create the Core Data stack (each user has their own local SQLite database). Following this approach we bring the stack up in a background thread, displaying an activity indicator to the user to let them know the app is doing something. If for some reason we can’t create the stack we then navigate to a “fatal error” view. Otherwise we notify the coordinator’s delegate (the AppCoordinator) that the stack is ready, which takes us to the…
- SplitViewCoordinator: this is the “main” coordinator for the app, as the rest of the app’s functionality is currently based from here. This coordinator sets up more specific dependencies like the note editor. It uses the delegate pattern to let the AppCoordinator know when the user’s session has ended, either voluntarily or via expiration, which removes the need for the nasty notifications we saw earlier. It loads the initial app view from a storyboard, and transitions from the DatabaseCoordinator‘s loading view to the app’s familiar “home” view.
By implementing the above pattern we completely removed the issues we saw earlier. We’re no longer tracking state between theoretically isolated areas of the app; dependencies are created and stored as, and where, they are needed; and the modular child coordinators lend themselves to the delegate pattern, which is a more specific, deterministic approach than relying on notifications.
Things we’re not yet sure about
The biggest problem we’ve had since switching to the coordinator pattern is deciding where to draw the line. As I mentioned, the SplitViewCoordinator is currently the last coordinator in the chain; from that point on the rest of the app is driven by it. As the app grows, this coordinator is instantiating, configuring, and storing more and more service objects which will be used by upcoming view controllers.
We regularly ask ourselves if the SplitViewCoordinator should have child coordinators of its own. The answer to this question is probably yes, but we’re not yet convinced of the best way to do this. We did try creating a new child coordinator per “scene” but the amount of code, and the complexity involved in the chain of delegates that resulted, was a bit overwhelming. As the app can arrive at the same scene a multitide of ways we often ended up with four or five child coordinators all linked to one another, which got a bit crazy.
It’s probably the right approach, but we need time to digest and consider how to implement it in a way that avoids us ending up with convoluted code.
Related to this, another decision we had to make was how to integrate this approach with segues between view controllers. Do we let the view controllers handle the segues as they were before, or should we call out to the coordinator acting as a delegate, and let the coordinator perform the segue, with context provided from the view controller and passed along the delegate method call?
Again that seems like the most elegant solution, but in practice it led to lots of verbose code. For now we’re letting the storyboards / view controllers manage navigation within the app, and using some aspect-oriented programming to inject dependencies so the view controllers don’t know too much about one another.
Dependency management in fact became a more pressing issue once we had the basic coordinators written, as 80% of the coordinators’ code was still concerned with instantiating dependencies for other services to use. With significant wins under our belt and time needed to process how we can take the coordinator pattern further, we next looked to how we could absolve the coordinators of this responsibility. My next post will look at options for dependency injection on the iOS platform and discuss the pros and cons of the three libraries we tried out.