iOS Multiple Window Support Without Storyboards

Supporting multiple windows and the UIScene lifecycle API programtically

If, like me, you prefer doing things programatically, instead of using interface builder, then the Main.Storyboard file doesn't last beyond creating the new project.

However, now with iOS 13 and multiple window there is now a SceneDelegate as well as an AppDelegate to deal with.
Fortunately, it isn't much more complex than before, just moved around slightly.

AppDelegate (iOS 12)

With iOS 12 you would setup an application programatically by deleting the Main.Storyboard and then creating the window in the AppDelegate's application:willFinishLaunchingWithOptions: or application:didFinishLaunchingWithOptions:.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    self.window = UIWindow(frame: UIScreen.main.bounds)
    self.window.rootViewController = ViewController()
    self.window.makeKeyAndVisible()
    return true
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = [[ViewController alloc] init];
    [self.window makeKeyAndVisible];
    return YES;
}

SceneDelegate (iOS 13)

The info.plist file still has the UIMainStoryboardFile key-value pair, but now it also has a UISceneStoryboardFile key inside the UIApplicationSceneManifest -> UISceneConfigurations array.

Once those 2 are deleted, the Storyboard is no longer involved in creating your apps UI and you need to do that in code yourself.
The location to do so is now in the SceneDelegate's -scene:willConnectToSession:options: method.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    guard let windowScene = (scene as? UIWindowScene) else { return }
    
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = ViewController()
    self.window = window
    window.makeKeyAndVisible()
}
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
    self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene*)scene];
    self.window.rootViewController = [[ViewController alloc] init];
    [self.window makeKeyAndVisible];
}

The only difference compared to the old -application:willFinishLaunchingWithOptions: code is that we now need to tell the UIWindow which scene it is part of. This can be done using the new UIWindow(windowScene: windowScene) init method, but windows can also be dynamically moved between scenes so there is also a windowScene property on UIWindow instances which can be changed at any time.