Coordinator: the missing pattern in UIKit
I am having so much benefit from using this pattern in last few years, I can’t help myself. I have to try and persuade all of you to use it too.
I’ve been evangelizing the use of Coordinator pattern for last two years or so, to anyone that would listen. At the same time, I continually improved and used my open source Coordinator implementation in actual live apps, some of which are fairly complex. Main goal was to integrate it so firmly into UIKit that it feels not like 3rd party code but like it actually is UICoordinator
.
Over time I realized the main hurdle for people is simply picturing why they need it and where does it fit into UIKit.
So my ask for today — give me 15-ish minutes of your time and read on.
Where does Coordinator pattern fits in the UIKit?
The two main building blocks of iOS app’ UIs are UIView and UIViewController.
UIView
is the canvas, the tactile surface. It’s where your app presents stuff and it gets user (inter)actions back. Or it seems so, because that’s not really true: in iOS, apps do not have ownership of the screen. iOS runtime does.
Entire UIKit is built on this defining characteristic (from developer point of view): you neither control the input nor directly receive the output. You get UIWindow
instance from the iOS and you add various UIView
subclasses to it but they will actually be drawn and shown as iOS decides to do so. Not at the moment when you added them. Nor can you force them to be visible, since iOS can decide to replace parts or all of your window with something else (notifications, phone calls etc).
In the same vein, you will indirectly be notified that something happened on those views either through UIResponder
methods and properties or by implementing various delegate protocols like UITextFieldDelegate
, UITableViewDelegate
etc. Or by employing target-action pattern of UIControl
which is essentially the same thing — post-festum delivery of the results and actions that already happened.
So when you look at the whole data-input → display → interaction & output chain, you as developer only get access to certain links in that chain. The chain points are obvious in the input direction — you set properties and call methods. Output direction is a bit foggy — stuff reaches your chain links through UIResponder, which is parent class of both UIView and UIViewController. That fact alone, should give you a hint: UIViewController
is an extension of UIView.
It always has one associated UIView
instance – if you remove that instance, UIVC is useless. It can’t exist and it can’t work and immediately crashes. Because of this, you can’t look into these two as separate entities. They are one.
UIViewController is part of the input chain for the UIView. It feeds data and/or other views into its .view
. It is also part of the output chain since through UIViewController is how you get notified when was view loaded by iOS, when is about to appear, when it actually appeared etc.
Each pairing of UIViewController
and UIView
represent one UI unit in the overall UI of the app. One self-containing piece of UI.
Missing bit here is where the data is coming from. Is UIViewController fetching the data or is being given the data it should display? The onus here is entirely on the iOS developer: your UIViewController should never directly fetch data.1 No network access, no data access (within the bounds of common sense).
UIViewController
, in nutshell, is very clear and straight-forward implementation of MVC pattern: it is mediator between data of any kind and one UIView
.
It has two distinct roles:
- receives data and configure / deliver it to the
.view
(or its subviews it knows about) - responds to / handles actions and events that occurred in the
.view
and/or its subviews - routes that response back into data storage or into some other UIViewController instance.
No, I did not make off-by-one error.
That 3rd item should not be there but it unfortunately is; in the form of show, showDetailViewController, performSegue, present & dismiss, navigationController, tabBarController etc. These methods and properties should never be found inside your UIViewController code.
That UIKit has thrown all that pile of navigation and routing responsibilities into UIViewController is its most serious architectural mis-step.
Content UIViewController instance should not care nor it should know about any other instance of UIVC or data sources, network, API etc.
Container UIViewController instance should know about its direct children UIVCs but it should ignore their
.view
s.
UIVC instance should only care about having an input (data) and output (events and actions).
- It does not care who/what sent that input.
- It does not care who/what handles its output.
Coordinator is the missing piece here. It is an object that should handle all that outside stuff. It is an object which
- instantiates UIVCs
- feeds the input into them
- receives the output from them
It order to do so, it also:
- keeps references to any data sources in the app
- implements data and UI flows it is responsible for
If you imagine your app’s UI as a house then each UIView/UIViewController pair is like one room in it: surface of the walls with paintings and cabinets and chairs and power plugs and light switches and anything else you may have visible in a room. Stuff you can interact with. Coordinators are the wiring and plumbings and (possibly) doors and windows – stuff that interconnect one room with other rooms in the house.
Why I think my Coordinator implementation is the best fit for UIKit apps?
Because I made it a subclass of UIResponder
+ minimally extended UIResponder and UIViewController in order to add some plumbing between UIViewController
and Coordinator
classes.
That same plumbing allows you to send a method call upwards through the output chain of UIView and UIViewController subclasses that is guaranteed to travel all the way up to the UIWindow
and UIApplicationDelegate
, which UIKit already does for some of its functionality.
When you call show(vc)
2 inside any UIViewController, it will magically bubble up to say UINavigationController
container which will do push
and execute its familiar slide from the right3 side. It’s irrelevant where UIVC which called show(vc)
is located in the UI stack — is it direct child of UINC or is maybe embedded 3 levels deeper. It will bubble up all the way to UINC. This magic is sadly available only in UIViewControllers and for few of its methods like show
and showDetailViewController
.
So what I did is extended this super-power to UIView
and my Coordinator
class as well, by piggy-backing on UIResponder existing behavior and supplementing it so you can define any method you want and have it callable from anywhere in the UI stack.
Thus by declaring a method like this:
extension UIResponder {
@objc dynamic func accountLogin(username: String,
password: String,
onQueue queue: OperationQueue? = nil,
sender: Any?,
callback: @escaping (User?, Error?) -> Void)
{
coordinatingResponder?.accountLogin(username: username,
password: password,
onQueue: queue,
sender: sender,
callback: callback)
}
}
you can
- Call
accountLogin()
from anywhere: view controller, view, button’s event handler, table/collection view cell, UIAlertAction etc. - That call will be passed up the responder chain until it reaches some Coordinator instance which overrides that method. If none does, it gets to UIApplicationDelegate (which is the top UI point your app is given by iOS runtime) and nothing happens.
- Through the
callback
closure, Coordinator can pass the results back down the chain. - At any point in this chain, not just the Coordinator, you can override this method, do whatever you want and continue the chain (or not, as you need).
There is no need for Delegate pattern (although nothing stops you from using one). No other pattern is required as well but you can use them if you wish / need to.
By reusing the essential component of UIKit design — the responder chain — any sort of data can travel up and down the coordinatingResponder
chain. No downsides of any kind, only benefits.
Imagine a typical login scenario that some AccountCoordinator may implement:
- Create an instance of LoginViewController and display it.
- Receive username/password from
LoginViewController
through the mentionedaccountLogin()
method. - Send them to
AccountManager
which is some non-UI object keeping track whatUser
is logged in. - If
AccountManager
returns an error, deliver that error back toLoginViewController
- If
AccountManager
returns a validUser
instance, replaceLoginViewController
withUserProfileViewController
In this scenario, LoginVC does not know that AccountManager exists nor it ever references it directly. It also does not know that AccountCoordinator nor UserProfileVC exist. It only cares about its input (optional AccountError
instance) and internally managing few private UITextFields and UIButton and some UILabels.
AccountCoordinator does not care how LoginVC works. It has no idea about any controls inside it. It also has no idea what AccountManager
does inside its login(user, pass, callback: ...)
method. Is it using URLSession, accessing Core Data stack — it does not know nor it cares. It’s just plumbing, the conduit between input and output points of LoginVC and AccountManager.
AccountManager, for its part, does not care about neither LoginVC nor AccountCoordinator. It does not know they even exist. It has a method which takes two Strings and a callback argument which is a closure with User and AccountError as arguments. It does not care who calls that method and who is actually owning that closure it should execute.
Coordinator pattern is the missing piece in iOS SDK. It enables loose or no coupling at all between otherwise unrelated objects (like LoginVC and AccountManager).
Clear input + clear output for any data or UI layer in your app. When everything is so boxed-in, then it’s easy to maintain and unit test, is it not?
Give the Coordinator micro-library a fair try. It has decently complex demo app showcasing all that’s relevant.
I don’t even use it as CocoaPod although you can use it that way. It’s only 4 short Swift files (it’s heavily-commented) hence I usually just copy the Coordinator folder directly into my project.
It is useful in apps of any size but it truly shines in really large apps, with dozens and dozens of UIViewControllers, multiple back-ends and APIs. It’s perfect tool to handle deep linking to any screen in the app, route push notifications, Handoff requests and what not. I really wish it exists as part of UIKit.
Comments and questions and pull requests are welcome on GitHub or on Twitter.
-
This was the catalyst for my infamous rant from last year. ↩︎
-
Architectural problem I mentioned earlier with
show(vc)
method is not the method itself but thatvc
instance — the fact that you need to instantiate some other VC and populate its input data which means having access to that data in the first place. ↩︎ -
Trailing side, more precisely speaking. ↩︎