Coordinators with Storyboards
I recently discovered Soroush Khanlou's NSSpain presentation on Coordinators, and once more I'm architecturally smitten. In short (and I'm being free and easy with the reality and details here, I admit), a view controller should not know about it's place in the hierarchy, therefore a coordinating object is used to push and pop view controllers as the need arises.
For all the details I recommend the video, or Soroush's blog post, or another post by Krzysztof Zabłocki where he writes about what he calls Flow Controllers, but the idea is more or less the same.
I got very excited. I strive to get things as architecturally 'correct' as possible. Obviously there's no such thing as a universally accepted definition of correct, but when smart people talk, you shut up and listen. I became a convert to the idea of coordinators.
There may be trouble ahead
As a developer there are a number of things I like, chief among them being lazy. I also love trying to be as precise as possible. But those two things can frequently be incompatible. With coordinators I had the opportunity to raise my architectural game to a level of precision I'd previously been completely unaware about. This I like.
Another thing I like, and it appeals to my laziness, is using Storyboards. This is not a massively popular view point, I'm beginning to suspect. After interviewing more than my fair share of candidates I discovered there was a lot of hostility to them, sometimes because of old school preferences for XIBs, other times because of ill informed and just plain incorrect repetition of how you couldn't have more than one Storyboard and this was bad for teams because of the conflicts.
So, I was a bit bummed out when browsing fellow coordinator convert Ayaka Nonaka's twitter feed to read that she had to choose between coordinators and storyboards because the storyboard requires too much view controller coupling, which obviously is entirely against the whole point of coordinators and the raison d'être of the storyboard.
Nuts.
So, that's it, then?
If you use Storyboards, you will call performSegueWithIdentifier(_:sender:) at various points in your view controller. You will also probably have your view controller's prepareForSegue(_:sender:) called as well. Apart from that, though, there really isn't anything else tying you to the storyboard. So, what if we could get our coordinator to take the place of those two methods? That alone would go a long way towards decoupling view controllers.
The coordinator is, in essence, the hierarchy of your app. It's the logic for the flow from one controller to another.
And if you swap "storyboard" for "coordinator' in the previous paragraph? Same thing, right? Or at least, same-ish enough to play around with.
Let's first look at the architecture for the coordinator
The coordinator is effectively a bus. It creates and launches a view controller, it receives callbacks from the view controller and, based on those callbacks, may create and present the next view controller in the flow.
In code we might model this as follows
Our view controller has a delegate, which it calls when a button is tapped or something else triggers a change. Our coordinator, which is the delegate, has knowledge of the flow and acts accordingly.
Now lets look at the storyboard
Hang on a second, you're thinking, this isn't what a storyboard looks like. It has view controllers hooked up to other view controllers. It has a visual layout of the app. It has an annoying tendancy to modify my files just because I looked at them. That's what a storyboard is. What I've put up here is the same coordinator diagram with the word "Storyboard" in its place. Well, yes. But this isn't the storyboard you see, this is the logical flow.
Ok, I admit, I'm leaving out a lot of detail here. UIStoryboard doesn't just create view controllers, it also creates the segue objects that controls the transitions from one view controller to another. And it's the segues that are the root of the coupling between the view controllers. Except we don't have to think of them that way.
Ideally we want to go from this:
To this:
To achieve this I'm going to treat the coordinator as a callback in the segue. In order to do this I'm going to rewrite my coordinators and view controllers slightly
This is beginning to look shaky. My CoordinationDelegate is being given the source and the destination view controller. This is breaking the coordinator pattern. But not to worry, the coordination delegate is now the second step of a two part coordination pattern. Don't forget, the destination view controller isn't coming from the source, it's coming from the segue.
So what we've got to do now, is to make the segue (the first step in the coordination) call the coordinator delegate (the second step) instead of asking the source view controller to prepareForSegue(_:sender:). We could do this by creating custom segues, which wouldn't be all that bad, but we'd have to create separate ones for navigation presentations, modals, popovers etc.
Instead we're going to take advantage of the fact that UISegue is an Objective-C class, instead of creating a new segue with a custom perform() method, we'll swizzle UISegue to get it to handle the coordination step.
This code needs a quick explanation. the class method addCoordination() needs to be called in order to swap the perform() implementation with swizzledPerform(). This we can do on app startup. Once that's done any segue that is triggered in the storyboard will call the source view controller's delegate to handle the specifics of the transition from the source to the destination. This means that the storyboard/segue pair have coordinated the flow from the source to the destination view controllers, the source knows nothing about the destination view controller.
Additionally, if the destination view controller conforms to the Coordinated protocol, it is given the same coordination delegate as the source. This can be overwritten in the delegate if necessary, but it maintains the architecture of the coordinator acting as the bus.
You'll notice that in the code above the coordination delegate was never set, we left that out. It's time to address that right now. We use Soroush's AppCoordinator pattern to instantiate the initial view controller with its Coordinator, as well as set up the swizzling for the UISegues.
Now, the final part, those pesky calls to performSegueWithIdentifier(_:sender:). This is the cheating part. Yes, performSegueWithIdentifier(_:sender:) is a UIViewController method, but instead of calling it in the view controller it is called in the coordinator.
Here's what that looks like in code, in the final version
And there we have it. In the example above where a button has been tapped the source view controller's @IBAction calls its coordinator without any knowledge of what that tap signifies. The coordinator triggers the segue, which will be created by the storyboard, which in turn will create the destination view controller and present it onscreen. The source view controller is at this point completely isolated from the view hierarchy, while coordination is provided by a module consisting of the storyboard, its segues and a very lightweight coordinator object.