Separation of concerns: UI Edition
Note: Sample project at the end.
I can be a little OCD when it comes to separation of concerns. Thankfully, for service level objects such as networking or persistence, this is easily achievable. For more complex objects that need other objects to function we have patterns like dependency injection to keep things clean.
When we move into the UI layer how can we keep this rolling? Well, a common (and relatively simple) pattern to help is MVVM. This allows us to move the logic out of our view controllers but let’s go further…
If you are already breaking down your interface into smaller custom UIView components then high five! There are, however, two elements of the view layer that can be harder to separate which I will focus on here. They are navigation and table/collection view cells.
What’s the Problem?
Firstly there’s navigation, if you use storyboards you are likely using segues:
The downsides to this from my point of view are:
- Segue identifiers are ‘stringly’ typed (we can start to eliminate this with enums).
- Source view controller is coupled to the destination view controller.
- There is a lot of boilerplate involved to unwrap and figure out which view controller is coming next. This only gets worse the more cases you have to handle.
The alternative when using nibs is generally to access the navigation controller directly:
The downsides to this approach are:
- Again, the source view controller is coupled to the destination view controller (and view model in the case of MVVM).
- View controllers have to have knowledge of their container and are responsible for deciding which operation to perform (push/present).
Secondly, we have table and collection view cells… here is a somewhat contrived example of a view controller containing a table view. The table view displays one of two types of cell depending on the item in the underlying data. A common way of handling this might be:
I have omitted code for clarity but this example highlights one pattern seen in a lot of table views. Here we see that we need to register each cell we intend to use. Next we need to use a switch (or a standard if/else) to handle the dequeuing and selection of cells.
And the downsides here are:
- The view controller is coupled to each type of cell it needs to show.
- This code is cumbersome to maintain; each time a new cell type is required we have to remember to update code in at least three different places.
- In the case of the switch pattern we need a default case — this is a code smell because the compiler can no longer tell us when the switch is not exhaustive.
Can we do better?
Here is our example app in action. It has the following components:
- Home view controller containing a table view.
- Two different cell types.
- Two view controllers that are shown depending on which cell is tapped.
So what if I told you that, within the view layer, none of those components interact directly at all… in fact they don’t even know the others exist!
I’m going to show you how to achieve this with the use of a navigation coordinator, protocols and MVVM.
NOTE: I just want to quickly mention that while it is outside the scope of this article to explain MVVM I have made the MVVM implementation for this project as simple as possible. It’s only using simple closures to handle things so don’t worry — there’s no ReactiveCocoa or RxSwift to try and understand this time.
This is the simplest step. Instead of performing navigation directly, the view controller will notify the view model that some ‘event’ has occurred. A trimmed down example looks like this:
Now our view controller almost acts like other ‘callback’ based components. With this tiny change we move the responsibility away from the view layer and we are free to do whatever we want with it. For example we can funnel it through to our…
This is another very simple object and the only one that has knowledge of all the screens in our project. Its sole purpose is to listen to the events from our view models, such as the one above, and perform the appropriate navigation. It looks something like:
Now we could actually stop here and you would have all your view controllers very nicely separated and completely ignorant of each other but let’s bring it home, it’s time to deal with those pesky…
For the purpose of this article I’m going to focus on table view cells, but the following can easily be adapted to consider collection view cells also. If you remember from earlier we saw that there are three things that we do with almost every cell we use, those are:
- Registration: the table view needs to know what cells it will be using.
- Dequeue/Creation: we need to provide the right cell for the right indexPath.
- Selection: cells, more often than not, perform some action when tapped.
Hmm, so we have a common set of functionality that we would like to extract?… time for a protocol!
Here is what we end up with:
Next we make the view models for our cells conform like so:
Here you can see that an extension on the cell view model now handles the registration and creation of the cell and, much like our view controllers, it also raises an event upon selection that our navigation coordinator listens for.
Finally we can update our view controller and replace the old table view code to use our new protocol:
Now it doesn’t matter how many different cell types we use, as long as the view model provides objects conforming to CellRepresentable it will ‘just work’.
So what have we achieved?
- Our view controllers are no longer responsible for navigation and now function as stand alone components.
- Our navigation has been centralised, we can now handle any number of navigation scenarios without the individual view controllers having to contain that logic.
- Our table/collection views now no longer need to handle cells directly. We have essentially taken the existing, centralised switch/if/else statements and used our new protocol to route the code to a single manageable location.
That’s it, that’s all.
I have thrown together a complete project to showcase these ideas on GitHub, you can check it out here.