Coordinator
Implementation of Coordinator design pattern. It is the application architecture pattern for iOS, carefully designed to fit into UIKit; so much so it could easily be UICoordinator
.
Since this is core architectural pattern, it’s not possible to explain its usage with one or two clever lines of code. Give it a day or two; analyze and play around with the demo example. I’m pretty sure you’ll find it worthy of your time and future projects.
Installation
Manually
My preferred method is to integrate Coordinator into the project manually. Just drag Coordinator
folder into your project — it‘s only four files.
If you prefer to use dependency managers, see below. Releases are tagged with Semantic Versioning in mind.
CocoaPods
CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Coordinator into your Xcode project using CocoaPods, specify it in your Podfile
:
pod 'Coordinator', :git => 'https://github.com/radianttap/Coordinator.git'
You must use direct link through :git
since CocoaPods central repository contains a framework of the same name. That framework seems long abandoned and my requests to remove it and thus allow me to publish mine were unsuccessful.
Look into Example-CocoaPods
folder for an example app using CocoaPods as dependency manager.
Setting up with Carthage
Carthage is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application.
You can install Carthage with Homebrew using the following command:
$ brew update
$ brew install carthage
To integrate Coordinator into your Xcode project using Carthage, specify it in your Cartfile
:
github "radianttap/Coordinator"
Coordinator.xcodeproj
in the root is using Carthage for the example app and is also housing the framework itself.
Coordinator: the pattern
(and why you need to use it)
UIViewController
, in essence, is very straight-forward implementation of MVC pattern: it is mediator between data model / data source of any kind and one UIView
. It has two roles:
- receives the data and configure / deliver it to the
view
- respond to actions and events that occurred in the view
- route that response back into data storage or into some other UIViewController instance
No, I did not make off-by-one error. The 3rd item should not be there but it is in the form of show, showDetailViewController, performSegue, present & dismiss, navigationController, tabBarController etc. Those methods and properties should never be found inside your UIViewController code.
UIViewController instance should not care nor it should know about any other instance of UIVC or anything else.
It should only care about its 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 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
- manages UI screens related to those flows (one screen == one UIVC)
Example
Login flow that some AccountCoordinator may implement:
- create an instance of LoginViewController and display it
- receive username/password from LoginViewController
- send them to AccountManager
- If AccountManager returns an error, deliver that error back to LoginViewController
- If AccountManager returns a User instance, replace LoginViewController with UserProfileViewController
In this scenario, LoginVC does not know that AccountManager exists nor it ever references it. It also does not know that AccountCoordinator nor UserProfileVC exist.
How can that be possible? Read on.
Coordinator: the library
In my library, Coordinator instance is essentially defined by these two points:
· 1 · It always has one instance of UIViewController which is its root ViewController. This is usually some container controller like UINavigationController
but it can be any subclass.
This way, it can internally create instances of UIVC, populate their input with data it needs and then just show or present them as needed. By reusing these essential UIKit mechanisms it minimally interferes with how iOS already works.
Coordinator takes care of navigation and routing while View Controller takes care of UI controls, touches and their corresponding events.
· 2 · It subclasses UIResponder
, same as UIView
and UIViewController
do.
This is crucial. My library extends UIResponder by giving it a new property called coordinatingResponder
. This means that if you define 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. It 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 then pass the results back down the chain. - At any point in this chain 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, ever.
By reusing the essential component of UIKit design — the responder chain — UIViewController's output can travel up and down the…
CoordinatingResponder chain
This is all that’s required for the chain to work:
public extension UIResponder {
@objc public var coordinatingResponder: UIResponder? {
return next
}
}
That bit covers all the UIView
subclasses: all the cells, buttons and other controls.
Then on UIViewController
level, this is specialized further:
extension UIViewController {
override open var coordinatingResponder: UIResponder? {
guard let parentCoordinator = self.parentCoordinator else {
guard let parentController = self.parent else {
guard let presentingController = self.presentingViewController else {
return view.superview
}
return presentingController as UIResponder
}
return parentController as UIResponder
}
return parentCoordinator as? UIResponder
}
}
Once responder chain moves into UIViewController instances, it stays there regardless of how the UIVC was displayed on screen: pushed or presented or embedded, it does not matter.
Once it reaches the Coordinator’s rootViewController
then the method call is passed to the parentCoordinator
of that root VC.
extension UIViewController {
public weak var parentCoordinator: Coordinating? {
get { ... }
set { ... }
}
}
So this is how the chain is closed up. Which brings us to the…
Coordinator: the class
Coordinator
class is parameterized with UIViewController subclass it uses as root. (Coordinating
protocol exists mainly to avoid issues with generic classes and collections.)
open class Coordinator<T: UIViewController>: UIResponder, Coordinating {
public init(rootViewController: T?) { ... }
open override var coordinatingResponder: UIResponder? {
return parent as? UIResponder
}
}
Since apps can be fairly complex, each Coordinator can have an unlimited number of child Coordinators. So you can have AccountCoordinator, PaymentCoordinator, CartCoordinator, CatalogCoordinator and so on. There are no rule nor guidelines: group your related app screens under particular Coordinator umbrella as you see fit.
When you instantiate a Coordinator instance, you call start()
to use it and stop()
when it’s no longer needed.
In the Coordinator subclass, you’ll override the method you defined in the UIResponder extension and actually do something useful instead of just calling the same method on next coordinatingResponder.
Demo example
The best explanation for any architecture is demo app, which I built.
CoordinatorExample in this repository mimics a shopping app. It has a fairly complete implementation of the Layers architecture where Coordinator is one of the central elements.
Look into AppDelegate
where you can see that each app must have an instance of AppCoordinator
, which is main entry point into the app. It’s created and strongly referenced by the AppDelegate.
This Coordinator then spawns any other Coordinators you need, when you need them. AppCoordinator is the only one that’s always in memory.
Advanced features & tips
See in the demo how I use Section
and Page
enums to declare parts and screens of the app and thus hide internal implementation of each Coordinator from the rest of the app.
Section == one Coordinator instance.
Page == one screen inside a particular Coordinator.
NavigationCoordinator
This is the only concrete subclass my library offers and I encourage you to subclass it for your own needs. It uses UINavigationController
as root VC.
Entire app in the demo uses just one instance of UINC which is shared among all Coordinators.
AppDependency
A very simple conduit to keep all your low-level (non-UI) singletons in one struct, automatically accessible to any Coordinator, at any level, through adoption of NeedsDependency
protocol.
Message queueing
Some dependencies can take a while to setup, things like Core Data stack. Thus you may end up in situation where your UI is shown before DataManager or middleware is ready to respond.
You can queue the received coordinatingResponder method call:
override func fetchPromotedProducts(sender: Any?, completion: @escaping ([Product], Error?) -> Void) {
guard let manager = dependencies?.catalogManager else {
enqueueMessage {
[weak self] in self?.fetchPromotedProducts(sender: sender, completion: completion)
}
return
}
manager.promotedProducts(callback: completion)
}
...and wait until dependency is updated; then try again:
var dependencies: AppDependency? {
didSet {
updateChildCoordinatorDependencies()
processQueuedMessages()
}
}
See CatalogCoordinator
in the demo, fetchPromotedProducts()
method for an example.
Naming your coordinatingResponder methods
I use consistent naming scheme to group my UIResponder extension methods. All stuff dealing with shopping cart use cart
prefix. Same with account, catalog etc. Xcode’s autocomplete then helps to filter possible options when coding.
(Sometimes a little consistency and common sense is enough.)
OperationQueue
Note the use of OperationQueue.perform
static method.
Since UIKit mandates usage on main thread only, this allows you to suggest to the eventual action responder that it should execute callback()
on the given queue
.
If no queue is specified, it’s executed on the current queue.
Further reading
On my blog: Coordinator: the missing pattern in UIKit
Rough history of development, also on my blog. I did not come up with the library all at once, it was a gradual process as all good things are.
Mind map your app - slides from my talk at NSBudapest meetup in May 2018. Very rough video recording of that talk.
Soroush Khanlou: Coordinators Redux
Andrey Panov: Coordinators Essential tutorial
Matthew Wyskiel: Protocol-Oriented App Coordinators in Swift
Coordinators are fairly old pattern but it was Soroush who brought them under iOS developer spotlight in 2015. My library follows the core idea but employs a different implementation.
Bill Dudney, WWDC 2014, session 224: Core iOS Application Architectural Patterns
Andy Matuschak & Colin Barrett, WWDC 2014, session 229: Advanced iOS Application Architecture and Patterns
I file these two talks under essential education for any iOS developer. While not directly associated with Coordinator pattern, you should still carefully watch and listen to understand what it means to “fit inside iOS SDK”.
License
MIT, as usual.