Imagine a world in which you can commit and push your changes to a feature branch, create a pull request, and never having to fear a merge conflict on a storyboard.
The solution? Don’t use them.
Now let it be perfectly clear that I love storyboards, and I almost always use them. I am however always wary of merge conflicts since they’re a real hassle to solve and most of the time you end up doing your changes twice. One of the reasons I love storyboards is that they provide you with a way to delegate navigation logic to them. By executing a so called segue, you tell your storyboard to navigate to a specific view controller in a specific way. I.e.: present a
Now it would be a shame to have to revert to programming all navigation logic in all of your ViewControllers. So at AppFoundry we came up with a solution to this problem: DRYNavigationManager This is a framework for allowing you to create nice separate classes that handle all your “navigational” needs.
Now if you’re like me and you don’t like to read blog posts (you just want to get on with it), you can find the repo here: DRYNavigationManager
Let’s dive into just how the DRYNavigationManager came to be.
We tried to list all the things a NavigationManger would need to do for us if we’d like to delegate the navigation logic to it. This is what we came up with:
- Single point of contact We wanted to have one class that would handle all our needs. So we didn’t have to manage 20 classes, all responsible for some part of the process.
- Work similar but improved Similar to storyboards that is, and improved in the sense that we don’t want to implement callbacks for passing on parameters and such.
- Clean code, divided into compartments We wanted to avoid having one big class that handles all our navigation.
- Clear development time errors We wanted to provide a clear understanding of what was going wrong at every time, assuming you’re not all kick-ass developers.
- Maximum extendability Navigation is something very specific for each application and we wouldn’t want to impede your creativity
We knew what we wanted to make. Now we needed to figure out how we would implement it. We isolated four components: the manager itself, the navigation descriptors, the navigator and the navigation translator. This might sound like gibberish to you now, so allow me to explain…
This component is the single point of contact that we talked about earlier. It’s responsible for executing navigations. It will also return the earliest encountered error when a navigation or the creation of a descriptor fails.
This component is the representation of a navigation, it holds all the data that will be needed to execute a navigation.
This component is the workhorse. It will check if you have access to the navigation you are trying to execute. And execute the navigation itself, including the transfer of data between the originating and the consequent ViewController.
This component allows the NavigationManager to translate an identifier, represented by a string, and a list of parameters to a NavigationDescriptor. This is to allow a similar work method like storyboards, as they also use strings to trigger segues.
Now how do these all work together to provide you with a way of navigating throughout your application? First you would create a Navigator where you would implement an optional access check and the navigation itself, transferring the given parameters to the ViewController that will be navigated to. Then you would provide the NavigationTranslator with your identifier which in turn will serve the class of your Navigator. The setup is now complete. Finally, to execute the navigation, you would provide the NavigationManager with your identifier. This will in turn create a NavigationDescriptor and use that to create an instance of your Navigator to execute the Navigation.
Without going in to deeply on how we implemented the framework (trust me, it’s not rocket science), I would like to highlight some of the code to help you understand how to use it.
Let’s take a look at
#import "DRYNavigator.h" @protocol DRYNavigationTranslationDataSource; @class DRYNavigationDescriptor; @interface DRYNavigationManager : NSObject - (instancetype)initWithNavigationTranslationDataSource:(id <DRYNavigationTranslationDataSource>)navigationTranslationDataSource; - (instancetype)initWithNavigationTranslationDataSource:(id <DRYNavigationTranslationDataSource>)navigationTranslationDataSource navigatorFactory:(id <DRYNavigatorFactory>)navigatorFactory; - (void)navigateWithNavigationIdentifier:(NSString *)identifier parameters:(NSDictionary *)parameters hostViewController:(UIViewController *)hostViewController errorHandler:(DRYNavigationErrorHandler)errorHandler successHandler:(DRYNavigationSuccessHandler)successHandler; @end
You can create an instance of the class using the first method. As an argument it takes an instance of a
DRYNavigationTranslationDataSource which I will get to later. The last method initiates a navigation itself, with a navigation identifier as a starting point. This method will go through all the steps needed to execute a navigation. If you want to see in more detail what those steps are you are more than welcome to roam about in the code.
This is the first class you want and need to implement yourself as this will do the specific translation from your predefined navigation identifiers to your own Navigators. You have complete freedom as to how you want to organise this class, however you do need to conform to the
DRYNavigationTranslationDataSource protocol provided by the framework.
@import Foundation; @protocol DRYNavigationTranslationDataSource <NSObject> - (NSString *)classNameForNavigationIdentifier:(NSString *)navigationIdentifier; @end
As you can see, you’re not limited in your implementation of this class. An example here could be that you have a
*.plist file that contains the mapping from an identifier to a classname. You could even imagine doing a call to the backend to retrieve that information. This could allow you to dynamically change the navigation within your app.
Last but definitely not least, the business end of the setup are all your navigators. You would create such a class for every navigation you want to do. Granted this will create a lot of classes even for a simple app, but the advantage you have here is that they are nicely separated from each other and you can organise them in your project tree whatever way you like. When all else fails you can resort to
cmd + shift + o for finding the appropriate class. These classes also need to conform to a protocol provided by the framework, namely
@import Foundation; @import UIKit; typedef void (^DRYNavigationErrorHandler)(NSError *); typedef void (^DRYNavigationSuccessHandler)(); @protocol DRYNavigator <NSObject> - (void)navigateWithParameters:(NSDictionary *)parameters hostViewController:(UIViewController *)hostViewController errorHandler:(DRYNavigationErrorHandler)errorHandler successHandler:(DRYNavigationSuccessHandler)successHandler; @end
This protocol requires your class to have a navigation method. This method needs to navigate to the destination ViewController. You can, for instance, program a push from the
navigationController of the
hostViewControlleror an other style of presentation. The
hostViewController in this context is the viewController that initiates the navigation.
You also have the option to conform to
@import Foundation; #import "DRYNavigator.h" @protocol DRYSecureNavigator <DRYNavigator> - (void)hasAccessWithParameters:(NSDictionary *)parameters errorHandler:(DRYNavigationErrorHandler)errorHandler successHandler:(DRYNavigationSuccessHandler)successHandler; @end
This protocol extends
DRYNavigator with another required method that would need to check if navigation can be executed. So, are all required parameters available? Does the initiator/user have access to the component?
The choice is up to you, do you just want to navigate or do you want to force an access check in advance.
Well, I hope things are pretty clear by now. You can see that all of the requirements are pretty much delivered upon. #winning
We’ve created something to assist you in creating clean code concerning navigation. If this isn’t your cup of tea, no hard feelings. If you have any questions, remarks or just want to tell us how we should have solved this in any other way, you are more than welcome to comment on this post. We’ll try to get back to you!
Thanks for reading