iosdev

Coordinator pattern in iOS 13 world

First let’s see how it was done before iOS 13. Then we’ll take a look how scenes changed this setup. Spoiler: quite a bit.

Before iOS 13, structure of an iOS app was really simple and straightforward:

① UIApplication
  · unowned(unsafe) open var delegate: UIApplicationDelegate?
    ⤷ ② AppDelegate
        · var window: UIWindow?
          · ③ open var rootViewController: UIViewController?

iOS runtime:

  1. creates an instance of UIApplication, then
  2. looks into your project for a class marked as @UIApplicationMain that implements UIApplicationDelegate protocol
  3. makes an instance of it and assigns as application.delegate and
  4. calls willFinishLaunching and didFinishLaunching methods to give you further control.

Hence the bare minimum needed to display your first UIViewController is this:

final class AppDelegate: UIResponder, UIApplicationDelegate {
	var window: UIWindow?

	func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

		window = UIWindow(frame: UIScreen.main.bounds)
		
		let vc = UIViewController()
		window?.rootViewController = vc

		window?.makeKeyAndVisible()

		return true
	}

The only “your” object guaranteed to last throughout the application lifecycle is AppDelegate. You could change VCs as you wanted but application’s delegate and its window were always there.

Now, to refresh our memory: how Coordinator fits in here? Essential role of the Coordinator is to manage UIViewControllers:

In simplest terms: it manages value of window.rootViewController property.

Logical place to inject Coordinator is between points ② and ③ in order to take over the job of setting up window.rootViewController.

final class AppDelegate: UIResponder, UIApplicationDelegate {
	var window: UIWindow?

	lazy var applicationCoordinator = ApplicationCoordinator(applicationDelegate: self)
	
	func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

		window = UIWindow(frame: UIScreen.main.bounds)
		window?.rootViewController = applicationCoordinator.rootViewController

		window?.makeKeyAndVisible()
		applicationCoordinator.start()

		return true
	}

I create strong reference from AppDelegate to ApplicationCoordinator thus guaranteeing that applicationCoordinator will live as long as the app.

final class ApplicationCoordinator: Coordinator<> {

	override func start(with completion: @escaping () -> Void = {}) {
		buildAppDependencies()
		super.start(with: completion)

		setupInitialContent() 
	}

And from there, whatever is shown as rootViewController of the ApplicationCoordinator will automatically be window.rootViewController as well. Responder chain is intact and Coordinator is ready to do its job of managing various UIViewControllers which are self-sufficient and unaware of each other.

AppDelegate remains oblivious to entire UI logic and complexity; it’s free to deal with whatever Apple throws in there (there’s plenty of that) and can pass any UI tasks to ApplicationCoordinator (like displaying proper UI for incoming push notification).

One useful tip for people already using my Coordinator library: injecting Coordinator like this will prevent coordinatingResponder methods to reach AppDelegate, which is needed to handle stuff like openURL(…). It’s easy to patch that up:

final class ApplicationCoordinator: Coordinator<> {
	weak var appDelegate: AppDelegate?

	override var coordinatingResponder: UIResponder? {
		return appDelegate
	}

Now you know why AppDelegate reference is passed to the ApplicationCoordinator.

From here-on, each project is unique. Some apps are simple and need only one Coordinator. Some are very complex and can have dozens of child Coordinators, each with one, two or dozen managed UIViewControllers. There’s really no limits what you can do, while keeping beautifully linear complexity curve.

BTW — have you noticed buildAppDependencies() in the ApplicationCoordinator’s start method?

Dependencies

In the KILS architecture, UI layer is dealing only with UI. Business logic implementation, data management, network services — all of those objects are independent of the UI and UI is unaware of them.

Those objects must exist somewhere in the app, somewhere that will guarantee they remain alive as long as the app is running. There are two possible places: AppDelegate or ApplicationCoordinator. Both are OK choices and before iOS 13 I usually chose the latter:

final class ApplicationCoordinator:  {
	var webService: 
	var accountManager: 
	var dataImporter: 
}

Coordinators coordinate between UI and middleware/data. To do that, each Coordinator instance must always have access to all those objects. I create simple struct with references to all of them and make sure that every time that struct is rebuilt, all active Coordinators in the app receive the updated instance:

struct AppDependency {
	var webService: 
	var accountManager: 
	var dataImporter: 
}

final class ApplicationCoordinator:  {
	var appDependency: AppDependency? {
		didSet {
			updateChildCoordinatorDependencies()
		}
	}
}

This is deceptively simple and very useful. Adding new services, middleware etc is straightforward.

iOS 13 & scenes

In iOS 13, Apple introduced possibility to have multiple active scenes (AKA windows). App architecture is a bit more complex now:

① UIApplication
  · unowned(unsafe) open var delegate: UIApplicationDelegate?
    ⤷ ② AppDelegate
  · open var openSessions: Set<③ UISceneSession>   
  · open var connectedScenes: Set<④ UIScene>

UIApplication still has its delegate which stays in memory as long as the app is alive. However, there is no UIWindow instance there any more; it’s moved to SceneDelegate.swift file, which implements delegate property of the UIScene which belongs to UISceneSession.

③ UISceneSession
  · ④ UIScene(.session)
      open var delegate: UISceneDelegate?
        ⤷ ⑤ SceneDelegate
            · var window: UIWindow?
               · ⑥ open var rootViewController: UIViewController?

“Belong” is a bit of a stretch; UIScene and UISceneSession are actually symbiotic, they always go together as 1-1 pairing, cross-referencing one another.

Here’s how app/scene lifecycle goes:

  1. iOS runtime creates an instance of UIApplication, then
  2. looks into your project for a class marked as @UIApplicationMain that implements UIApplicationDelegate protocol, then
  3. makes an instance of it and assigns as application.delegate, then
  4. calls willFinishLaunching and didFinishLaunching where you can do non-UI stuff (setup logging, metrics, dependencies etc).
  5. Runtime creates an instance of UISceneSession, then
  6. calls configurationForConnecting:UISceneSession on AppDelegate passing that UISceneSession instance, to give you a chance to supply custom UISceneConfiguration for it (otherwise it will use whatever is set in Info.plist).
  7. Runtime uses given scene configuration to instantiate UIScene and set that object as value of UISceneSession.scene property.
  8. Part of the scene configuration is the name of your class that implements UIWindowSceneDelegate (which inherits UISceneDelegate).

    By default that’s SceneDelegate so iOS runtime will instantiate that class and then
  9. call its scene(UIScene, willConnectTo: UISceneSession to give you further control.
  10. In that method you setup UIWindow instance for the given scene and assign some UIViewController as window.rootViewController

As you can see, step 4 has changed to not setup any UI. UI setup is done in steps 5–10.

Notice that UIApplication has separate properties called openSessions and connectedScenes. This hints that during app’s lifecycle these sets may not have same number of elements.

WWDC 2019’ session 258: Architecting Your App for Multiple Windows explains this in greater detail but to summarise important bits:

This last bit is important for Coordinators. Coordinator manages related UIVCs for one window, one scene. Thus natural place to inject them is between ⑤ and ⑥:

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
	var window: UIWindow?
	
	var coordinator: SceneCoordinator?
	
	func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
		guard let windowScene = scene as? UIWindowScene else { return }
		
		let window = UIWindow(windowScene: windowScene)
		self.window = window
		
		let sceneCoordinator = 
		self.coordinator = sceneCoordinator

		window.rootViewController = sceneCoordinator.rootViewController

		window.makeKeyAndVisible()
		sceneCoordinator.start()
	}
}

But what about AppDependency? It contains objects that should be common for an entire app, not have one instance of each, per scene. Service wrappers, data importers etc – these are usually singleton instances rest of the app uses as needed. Because SceneDelegate goes away with the UIScene, we can’t keep those singletons on it. They now must be kept on AppDelegate since it’s the only object we have that lives as long as the app does.

final class AppDelegate: UIResponder, UIApplicationDelegate {
	var webService: 
	var accountManager: 
	var dataImporter: 

	var appDependency: AppDependency? {
		didSet {
			updateSceneDependencies()
		}
	}

Each time dependencies change, all active SceneCoordinators must be updated through updateSceneDependencies(). But how? What’s the connection between AppDelegate and SceneDelegate? Well, there is none. They do not reference one another, ever.

UISceneSession instance is the only one we have access to, in AppDelegate, so we must inject AppDependency into it:

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {

	connectingSceneSession.appDependency = appDependency

	return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

How to inject this? In the same way we inject Coordinator into every UIViewController:

extension UISceneSession {
	private struct AssociatedKeys {
		static var appDependency = "AppDependency"
		static var sceneCoordinator = "SceneCoordinator"
	}

	var appDependency: AppDependency? {
		get {
			return objc_getAssociatedObject(self, &AssociatedKeys.appDependency) as? AppDependency
		}
		set {
			objc_setAssociatedObject(self, &AssociatedKeys.appDependency, newValue, .OBJC_ASSOCIATION_COPY)
			sceneCoordinator?.appDependency = newValue
		}
	}

	weak var sceneCoordinator: SceneCoordinator? {
		get {
			return objc_getAssociatedObject(self, &AssociatedKeys.sceneCoordinator) as? SceneCoordinator
		}
		set {
			objc_setAssociatedObject(self, &AssociatedKeys.sceneCoordinator, newValue, .OBJC_ASSOCIATION_ASSIGN)
		}
	}
}

We go one step further and also inject a weak reference to SceneCoordinator into UISceneSession too. Thus when appDependency is set, it will automatically set corresponding property on sceneCoordinator.

Hence the scene coordinator setup in SceneDelegate goes like this:

let sceneCoordinator = SceneCoordinator(scene: scene, sceneDelegate: self)
self.coordinator = sceneCoordinator

Here’s a typical implementation of SceneCoordinator:

final class SceneCoordinator: NavigationCoordinator, NeedsDependency {
	private weak var scene: UIScene!
	private weak var sceneDelegate: SceneDelegate!

	init(scene: UIScene, sceneDelegate: SceneDelegate) {
		self.scene = scene
		self.sceneDelegate = sceneDelegate
		
		let nc = UINavigationController()
		super.init(rootViewController: nc)

		appDependency = scene.session.appDependency
		scene.session.sceneCoordinator = self
	}

	override var coordinatingResponder: UIResponder? {
		return sceneDelegate
	}
	
	var appDependency: AppDependency? {
		didSet {
			updateChildCoordinatorDependencies()
		}
	}

	override func start(with completion: @escaping () -> Void = {}) {
		super.start(with: completion)
		
		displayContent()
	}
	
}

private extension SceneCoordinator {
	func displayContent() {
		let vc = FirstController()
		show(vc)
	}
}

In AppDelegate we need just this bit to close the updating chain for AppDependency:

private extension AppDelegate {
	func updateSceneDependencies() {
		let application = UIApplication.shared
		
		application.openSessions.forEach {
			$0.appDependency = appDependency
		}
	}
}

This finishes administrative parts of the Coordinator pattern. You are now free to do as before, using SceneCoordinator in the same way you used ApplicationCoordinator before iOS 13.

I created a skeleton repo which you can use as starting point for an iOS 13 app employing Coordinator pattern.

Consider it some sort of public beta. I have yet to use this implementation in multiple apps, especially those that truly use multiple windows. Practice will make this better.