Navigation in iOS apps has always been a source of pain and debates. A hundred of articles has already been written, so please welcome the 101-th.
Over the last few years I tried different navigation approaches and came to the conclusion, that there’s no silver bullet navigation mechanism which could get all your navigation needs covered. Some of them were a perfect fit for one app, but absolutely inappropriate for another. Oftentimes I found myself mixing up different navigation approaches in the same application, which made me sleep bad, frankly.
This is how at some point I started thinking about my own (the best ever) navigation approach. Well it is much easier to say than to do. First of all what is best for me may not suit others. Below there’s my own list of main principles a perfect navigation should comply with.
- Straightforward and easy to use public API. Ideally I’d like to be able to create a view controller, pass initial data and present it in just a single line. Now, I’m a multiple storyboards person, so supporting storyboards is a must.
- Type safety. I’d like to leverage the power of Swift type safety as much as possible. Please no more string view controller identifiers, string storyboard names and optional down castings.
- Simple to support and extend. As my application grows I’d like the routine of adding new navigation routes to be as simple as possible, so that I didn’t have a temptation to drop another segue on the storyboard.
In this article I’d like to introduce another way of handling navigation powered by protocol extensions.
For me navigation is a complex notion which is not just about presenting view controllers here and there. Let’s think about it as a huge step from view controller A to view controller B, which basically comprises a number of smaller sub-steps:
- Create an instance of a view controller, either from a storyboard or programmatically.
- Pass initial data to the view controller (optionally).
- Present the view controller on screen.
In code, it all usually boils down to something like this:
What’s wrong with this piece of code?
- Referencing the storyboard by a string name.
- Referencing the view controller by a string identifier.
- Down casting which may fail.
- After all it’s plenty of code.
Can we improve it? Let’s see…
We’ll start off by instantiation a view controller and passing initial data to it. Assuming we have a ProfileViewController with a viewModel property of type Profile, we’re gonna make it as simple as:
For making that happen, we could simply declare a static function on the ProfileViewController, but shall we make it more generic? Let’s first define a simple protocol, called Instantiatable:
And now let’s create an extension for this protocol constrained to UIViewController subclasses.
Here we use a static function getGenericViewController(), which is implemented in the UIStoryboard extension. Feel free to check the git repository for the implementation details and full source code. This function returns an instance of a UIViewController by trying to instantiate it from every single storyboard file found in the project. Then after instantiating a generic view controller we cast it to a concrete UIViewController subclass, set its view model and return a ready to use view controller. To make this happen we have to come to a small agreement — Storyboard IDs should match its view controllers classes:
And now let’s extend the ProfileViewController with our new protocol:
Now with the power of protocol extensions and a bit of generics magic we can easily instantiate any view controller conforming the Instantiatable protocol and populate it with a view model in just one line:
Since the view controller is ready, let’s take care of presenting it. When I was thinking about presentation strategy, my main goals were:
- Make it independent of view controllers names. Rather than presenting a ProfileViewCotroller, I’d like to present a profile.
- Make it decentralised. No more coordinators, sub-coordinators and sub-sub-coordinators.
- Make view controllers easy to present from multiple places in the app with no hassle.
And this is where the protocol extensions come in. First of all let’s think about presenting view controllers as of abilities or skills. For example, ability to present a user’s profile implies that someone lucky can open a profile, or ability to present a settings view, and so on. Then once we have a catalogue of those abilities we would simply endow our view controllers with any of them. Sounds intriguing? Let’s jump right into the code.
Above we defined a couple of such abilities: CanOpenProfile and CanOpenSettings. Eeach protocol is responsible for just a single navigation path, which opens up endless possibilities for composition. We’ll see it in action shortly.
I personally prefer to prefix them with Can to emphasise their application. For both of them we provided an extension constrained to UIViewControllers. This constraint actually allows us to use the common presentation techniques of UIKit. For this specific example I used UINavigationController, but you can easily extend it to support the modal presentation.
Finally it’s time to endow our view controllers with some of the newly created abilities:
And that is where it really shines. It’s that easy. By just extending a view controller with our ability protocols, we automatically endow them with the corresponding skills. Protocols composition in action! 🚀
Note the absence of any specific UIViewController subclass. We referred to neither ProfileViewController, nor SettingsViewController. Instead we put them to the protocol extensions — small independent pieces of our new navigation architecture.
We designed a simple yet powerful navigation solution which features Swift generics, protocol extensions and composition. Feel free to use it as a starting point and to extend it with additional features when you need to.
Thanks for reading.
For the full source code please refer to: