Chris Myers
Posted on December 13, 2021
It's been awhile since I set up a coordinator flow in a project. My previous project was well established and once the flow was set up to start the project, it was rarely modified.
For those that are unfamiliar with the Coordinator pattern, there are many blog posts regarding use and set up. These are some of my favorites to look at because I always need a refresher when setting up a new project.
- Soroush Khanlou - The Coordinator
- Soroush Khanlou - Coordinator Redux
- Paul Hudson - How to use the coordinator pattern in iOS apps
- Ian MacCallum - Coordinators, Routers, and Back Buttons
- Ray Wenderlich - Coordinator Tutorial in iOS: Getting Started
So imagine my surprise when I decide to create a new sample project last week and couldn't find var window: UIWindow?
anywhere in the AppDelegate. It's just not there.
The Old AppDelegate
The New AppDelegate
As it turns out, some of the delegate methods associated with the AppDelegate have been migrated over to a new file that comes out of the box when setting up a new project, SceneDelegate
when Apple released iOS 13. A wonderful overview of the differences in AppDelegate
and SceneDelegate
can be found here: Understanding the iOS 13 Scene Delegate
I strongly encourage reading the article, especially about changes to the info.plist
, which I won't cover in this post.
The SceneDelegate
The SceneDelegate
file now contains the window property.
var window: UIWindow?
Okay, no big deal, I'll set up my coordinator from here.
Still doesn't work
Well, looking at the developer documentation, the AppDelegate
still conforms to UIApplicationDelegate
, but the delegate methods have changed. It is still the file to use to register for Push Notifications, responding to those notifications, responding to events that target the app itself, but there is also a new area that allows you to configure the app scenes--specifically, connecting to and discarding SceneSessions.
The SceneDelegate
file conforms to UISceneWindowDelegate
, which itself conforms to UISceneDelegate
. The window
property is part of the UIWindowSceneDelegate
and the main window associated with the scene.
The associated functions in the SceneDelegate
are protocol methods from UISceneDelegate
. They handle what the app should do if the scene enters the foreground, goes to the background and handle any other life-cycle events occurring within a scene.
But getting back to the window: UIWindow
property, there is a new way to initialize this variable. In the past, initializing the window property in the AppDelegate
was something like this:
self.window = UIWindow(frame: UIScreen.main.bounds)
You can still set up your window this way. UIScreen.main.bounds
still is available. However, with the introduction to Scenes, Apple lets us know that there is a new, preferred way to initialize the UIWindow
.
Inside of the delegate method, func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
sits this comment:
Use this method to optionally configure and attach the UIWindow
window
to the provided UIWindowScenescene
.If using a storyboard, the
window
property will automatically be initialized and attached to the scene.This delegate does not imply the connecting scene or session are new (see
application:configurationForConnectingSceneSession
instead).
So instead of using UIScreen to initialize the window frame, this is what is expected:
- Step One: safely unwrap the provided
scene: UIScene
in the delegate method and cast it as aUIWindowScene
. - Step Two: Initialize the
window
property using the unwrappedUIWindowScene
. - Step Three: set the
windowScene
property now found in a UIWindow with the unwrappedwindowScene
from step one.
Step three is vitally important, because if this step is missed, you'll just get a blank screen when running the app.
In code, this looks like the following:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let uiWindow = UIWindow(frame: windowScene.coordinateSpace.bounds)
self.window = uiWindow
self.window?.windowScene = windowScene
}
Thus far, I haven't mentioned anything about the actual coordinator. I wanted to separate the two, because once the window initialization is set up, the coordinator piece is pretty straight forward.
First, start with a protocol... :)
protocol Coordinator {
func start()
}
Next, create an application coordinator conforming to the protocol. (I'm using the Ray Wenderlich blog post I linked above as my sample AppCoordinator)
class AppCoordinator: Coordinator {
let window: UIWindow
let rootViewController: UINavigationController
init(_ window: UIWindow) {
self.window = window
rootViewController = UINavigationController()
let mainVC = MainViewController()
rootViewController.pushViewController(mainVC, animated: true)
}
func start() {
window.rootViewController = rootViewController
window.makeKeyAndVisible()
}
}
After that, it's just a matter of modifying the previously mentioned delegate method in the SceneDelegate
.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let uiWindow = UIWindow(frame: windowScene.coordinateSpace.bounds)
self.window = uiWindow
self.window?.windowScene = windowScene
let appCoordinator = AppCoordinator(uiWindow)
self.appCoordinator = appCoordinator
appCoordinator.start()
}
I chose to set up my app without using storyboards. Just note, to remove references to the storyboard, the main storyboard can be found in the SceneConfiguration
in the plist
. Just deleting from the Main Interface
in the App Target isn't enough.
You can absolutely use coordinators with a storyboard (See Paul Hudson's article linked above) if you so choose.
Posted on December 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.