Using Coordinator With Scene Delegates

Sun Aug 25 2019 | Mark Struzinski

I have switched to using the coordinator pattern with any new work I do. I was resistant to break away from Storyboards using segues at first. But over the years I’ve learned any app of medium to large complexity breaks down under the weight of segues after a certain point.

I’ve been playing around with the new iOS 13 APIs a bit, and I was trying to create a new UIKit app using the coordinator pattern. The new UIKit template assumes the use of scenes instead of a single window. The single view app template breaks away from the standard app delegate and uses the new scene APIs. File ➤ New Project now creates a UISceneDelegate and wires everything up for you.

The Xcode template assumes you want to use storyboard from the application start. It creates a scene configuration that wires itself up to Main.storyboard. As you might know, this does not play well with the coordinator pattern. The coordinator pattern works best if your application starts up with a coordinator at the root.

I used to initialize my app coordinator in the app delegate. In the base project template, I would remove the starting storyboard entry. Then I would grab hold of the window property in the app delegate and initialize it with a nav controller. Then I would pass that nav controller into the app coordinator and control then goes to the coordinator for the rest of the work.

I haven’t dug into multiple window support yet, so this article assumes support for a single window and scene configuration. It took me a bit of research to get this working, so I thought I’d post it here in case anyone else has the same problem,

First, go to your app target on the General tab. In the Deployment Info section, remove the entry in the Main Interface field.

This will remove the UIMainStoryboardFile entry from the Info.plist. This prevents the app from launching into the storyboard directly.

Now open SceneDelegate.swift. The scene(_:willConnectTo:options:) method will initially look something like this (minus the boilerplate comments that are normally injected):

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let _ = (scene as? UIWindowScene) else { return }
}

You’ll need to add a couple of parameters at the top here. You need an App Coordinator to start your app, and a UIWindow to initialize your UI.

Add those parameters to the top of the SceneDelegate:

var window: UIWindow?
var appCoordinator: AppCoordinator!

Note:

The implementation of the app coordinator is outside the scope of this article. Assume it conforms to a protocol that ensures it has a start() method to bootstrap your UI.

Now, update the scene(_:willConnectTo:options:) method with the following:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // 1
    guard let windowScene = (scene as? UIWindowScene) else {
        return
    }
    
    // 2
    let appWindow = UIWindow(frame: windowScene.coordinateSpace.bounds)
    appWindow.windowScene = windowScene
    
    // 3
    let navController = UINavigationController()
    appCoordinator = AppCoordinator(navigationController: navController)
    appCoordinator.start()

    // 4
    appWindow.rootViewController = navController
    appWindow.makeKeyAndVisible()
    
    // 5
    window = appWindow
}
  1. Ensure the scene being passed in can be downcast to a UIWindowScene.
  2. Create a UIWindow and assign the UIWindowScene to it.
  3. Perform the standard maintenance of initializing the starting navigation controller. Pass this to the app coordinator, and call start() on the app coordinator.
  4. Set the navigation controller as the root view of the window. Make the app window visible.
  5. Set the window property that you created earlier.

That’s it! You now have a working coordinator, and application flow can proceed in the normal way dictated by the coordinator pattern.