Scene-based applications come with a few challenges for developers, one of them being dependency injection. How do you inject a dependency, such as a view model, into the root view controller of the window? That is the question I answer in this episode.

Understanding Scene-Based Applications

As of iOS 13, your application can choose to support multiple windows on iPad. To make that possible, Apple had to rethink the application and user interface life cycles on iOS. If you want to learn more about these changes and how they affect developers, I recommend taking a look at Understanding Scene-Based Applications. In this episode, I focus on dependency injection and scene-based applications.

Creating the Project

Fire up Xcode and create a project by choosing the App template from the iOS > Application section.

Creating a Scene-Based Application in Xcode

Give the project a name and make sure to set Interface to Storyboard. This episode doesn't apply to applications that use SwiftUI for their user interface.

Creating a Scene-Based Application in Xcode

Storyboards, XIB Files, or Code

Even though we use a storyboard in this episode, know that you don't have to. What you learn in this episode applies to projects that use storyboards, XIB files, or code for their user interface. Create a Swift file and name it ViewModel.swift and define a struct with name ViewModel. The struct defines a computed property, title, of type String.

import Foundation

struct ViewModel {

    var title: String {
        "Welcome to Cocoacasts!"
    }

}

The view model defines what the view controller displays. In this example, the view model defines the title the view controller displays.

Open ViewController.swift and define a private, constant property, viewModel, of type ViewModel.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    private let viewModel: ViewModel

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

The view model is injected into the view controller during initialization. Since we use a storyboard, we need to define an initializer that accepts an NSCoder instance as its first argument. The second argument is the view model. How the pieces fit together becomes clear in a moment.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    private let viewModel: ViewModel

    // MARK: - Initialization

    init?(coder: NSCoder, viewModel: ViewModel) {
        self.viewModel = viewModel

        super.init(coder: coder)
    }

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

We are also required to implement the init(coder:) initializer, a required initializer. We throw a fatal error in the body of the initializer since this initializer shouldn't be used to create a ViewController instance.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    private let viewModel: ViewModel

    // MARK: - Initialization

    init?(coder: NSCoder, viewModel: ViewModel) {
        self.viewModel = viewModel

        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError("Use `init(coder:viewModel:)` to instantiate a `ViewController` instance.")
    }

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

Creating the User Interface

The user interface of the view controller is simple. We display a label in the center of its view. Define an outlet for a label, titleLabel, in ViewController.swift.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    private let viewModel: ViewModel

    // MARK: -

    @IBOutlet private var titleLabel: UILabel!

	...

}

We set the text property of the label in the view controller's viewDidLoad() method by asking the view model for the value of its title property.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    titleLabel.text = viewModel.title
}

Open the main storyboard and add a label to the view controller scene. Add the necessary constraints to center the label in its superview, the view controller's view. Connect the label to the outlet we defined earlier.

Creating the User Interface in Interface Builder

Updating the Scene Delegate

Open SceneDelegate.swift. We need to make a few changes to the scene(_:willConnectTo:options:) method. First, we replace the _ in the guard statement with windowScene.

// MARK: - Scene Life Cycle

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

Second, we ask the main storyboard to instantiate its initial view controller. Notice that we pass a closure as an argument. The closure accepts an NSCoder instance as its only argument and its return type is UIViewController?. We use the NSCoder instance to create a ViewController instance, invoking the initializer we implemented earlier in this episode. We pass the NSCoder instance as the first argument and a ViewModel object as the second argument.

// MARK: - Scene Life Cycle

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

    // Initialize Root View Controller
    let rootViewController = UIStoryboard(name: "Main", bundle: .main).instantiateInitialViewController { coder in
        ViewController(coder: coder, viewModel: ViewModel())
    }
}

The remaining steps are straightforward. We create a UIWindow instance using the UIWindowScene instance, set the view controller as the root view controller of the window, and make the window the key window.

// MARK: - Scene Life Cycle

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

    // Initialize Root View Controller
    let rootViewController = UIStoryboard(name: "Main", bundle: .main).instantiateInitialViewController { coder in
        ViewController(coder: coder, viewModel: ViewModel())
    }

    // Initialize Window
    window = UIWindow(windowScene: windowScene)

    // Configure Window
    window?.rootViewController = rootViewController

    // Make Window Key Window
    window?.makeKeyAndVisible()
}

Build and run the application to see the result. The application should crash because the init(coder:) initializer of the ViewController class is invoked instead of the init(coder:viewModel:) initializer. Why is that?

A Fatal Error Is Thrown

We need to make one more change. Select the target's Info.plist and expand the Application Scene Manifest entry. Remove the entry with name Storyboard Name. If this entry is present, the application instantiates the initial view controller of the storyboard with that name and sets it as the root view controller of the window. That is not what we want.

Updating the Target's Info.plist

With the Storyboard Name entry removed, run the application one more time. The view controller should display the title the view model defines, which confirms the view model was successfully injected into the view controller.

Dependency Injection in a Scene-Based Application

What's Next?

As I mentioned earlier, the approach described in this episode also works for projects that use XIB files or define their user interface in code. We had to jump through a few hoops, but know that you only need to go through this setup once.