Mastering Navigation With Coordinators

Instantiating View Controllers From a Storyboard

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy

If you're familiar with Cocoacasts, then you may know that I'm allergic to string literals randomly scattered in a project. Open AppCoordinator.swift and navigate to the showQuotes() method.

private func showQuotes() {
    // Initialize Quotes View Controller
    guard let quotesViewController = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "QuotesViewController") as? QuotesViewController else {
        fatalError("Unable to Instantiate Quotes View Controller")
    }

    // Push Quotes View Controller Onto Navigation Stack
    navigationController.pushViewController(quotesViewController, animated: true)
}

We use a few string literals to instantiate the quotes view controller from the main storyboard. This isn't a major issue, but we can clean up the implementation using a protocol and a protocol extension.

Protocols and Protocol Extensions

The goal of this episode is clear, (1) rid the project of as many string literals as possible and (2) simplify the instantiation of view controllers to avoid code duplication. To accomplish that goal, we take advantage of a protocol and a protocol extension. This will result in a clean and elegant API without adding a lot of complexity.

Defining a Protocol

Create a new group, Protocols, and add a Swift file with name Storyboardable.swift. Add an import statement for the UIKit framework and define a protocol with name Storyboardable.

import UIKit

protocol Storyboardable {

}

The idea is simple. We should be able to instantiate a UIViewController subclass conforming to the Storyboardable protocol by invoking a static method with name instantiate(). Let's start by defining that method.

import UIKit

protocol Storyboardable {

    // MARK: - Methods

    static func instantiate() -> Self

}

Notice that the return value of the instantiate() method is of type Self, the type conforming to the Storyboardable protocol. The AppCoordinator class instantiates an instance of the QuotesViewController class in its showQuotes() method.

private func showQuotes() {
    // Initialize Quotes View Controller
    guard let quotesViewController = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "QuotesViewController") as? QuotesViewController else {
        fatalError("Unable to Instantiate Quotes View Controller")
    }

    // Push Quotes View Controller Onto Navigation Stack
    navigationController.pushViewController(quotesViewController, animated: true)
}

The implementation of the showQuotes() method shows that we need three pieces of information to instantiate the quotes view controller, (1) the name of the storyboard, (2) the bundle containing the storyboard, and (2) the storyboard identifier of the view controller.

// Initialize Quotes View Controller
guard let quotesViewController = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "QuotesViewController") as? QuotesViewController else {
    fatalError("Unable to Instantiate Quotes View Controller")
}

Let's define a property for each of these pieces of information. We define a static computed property storyboardName of type String, a static computed property storyboardBundle of type Bundle, and a static computed property storyboardIdentifier of type String.

import UIKit

protocol Storyboardable {

    // MARK: - Properties

    static var storyboardName: String { get }
    static var storyboardBundle: Bundle { get }

    // MARK: -

    static var storyboardIdentifier: String { get }

    // MARK: - Methods

    static func instantiate() -> Self

}

Creating a Protocol Extension

Every type conforming to the Storyboardable protocol needs to implement these computed properties as well as the instantiate() method. But a protocol extension can help us simplify the solution we're implementing.

We define a protocol extension for the Storyboardable protocol and use a generic where clause to require that Self is a subclass of UIViewController. Self refers to the type conforming to the Storyboardable protocol. This opens up several possibilities.

import UIKit

protocol Storyboardable {
    ...
}

extension Storyboardable where Self: UIViewController {

}

We use the protocol extension to provide default implementations for the computed properties and the instantiate() method. If a UIViewController subclass provides its own implementation, then that implementation is used instead of the default implementation provided by the protocol extension. We return Main for the storyboardName computed property and the main bundle for the storyboardBundle computed property.

extension Storyboardable where Self: UIViewController {

    // MARK: - Properties

    static var storyboardName: String {
        return "Main"
    }

    static var storyboardBundle: Bundle {
        return .main
    }

}

The default implementation of the storyboardIdentifier computed property returns the name of the class. We invoke the init(describing:) initializer of String, passing in self as the argument. In this example, self refers to the conforming type.

extension Storyboardable where Self: UIViewController {

    // MARK: - Properties

    static var storyboardName: String {
        return "Main"
    }

    static var storyboardBundle: Bundle {
        return .main
    }

    // MARK: -

    static var storyboardIdentifier: String {
        return String(describing: self)
    }

}

With the computed properties implemented, we can provide a default implementation for the instantiate() method. We load the storyboard using the storyboardName and storyboardBundle computed properties. The view controller is instantiated by invoking the instantiateViewController(withIdentifier:) method, passing in the storyboard identifier as defined by the Storyboardable protocol. We cast the result of instantiateViewController(withIdentifier:) to Self, the type conforming to the Storyboardable protocol.

We use a guard statement to instantiate the view controller. If the instantiation of the view controller fails, a fatal error is thrown. The view controller is returned if the instantiation is successful.

extension Storyboardable where Self: UIViewController {

    // MARK: - Properties

    static var storyboardName: String {
        return "Main"
    }

    static var storyboardBundle: Bundle {
        return .main
    }

    // MARK: -

    static var storyboardIdentifier: String {
        return String(describing: self)
    }

    // MARK: - Methods

    static func instantiate() -> Self {
        guard let viewController = UIStoryboard(name: storyboardName, bundle: storyboardBundle).instantiateViewController(withIdentifier: storyboardIdentifier) as? Self else {
            fatalError("Unable to Instantiate View Controller With Storyboard Identifier \(storyboardIdentifier)")
        }

        return viewController
    }

}

Conforming QuotesViewController to Storyboardable

Before we can use the instantiate() method of the Storyboardable protocol, the QuotesViewController class needs to conform to the protocol. Open QuotesViewController.swift and conform the QuotesViewController class to the Storyboardable protocol.

import UIKit

class QuotesViewController: UIViewController, Storyboardable {

    ...

}

Thanks to the protocol extension, we don't need to implement any of the computed properties and we don't need to implement the instantiate() method. Open AppCoordinator.swift and navigate to the showQuotes() method. We can remove the guard statement and simplify the implementation. We invoke the instantiate() method on the QuotesViewController class and assign it to the quotesViewController constant.

private func showQuotes() {
    // Initialize Quotes View Controller
    let quotesViewController = QuotesViewController.instantiate()

    // Push Quotes View Controller Onto Navigation Stack
    navigationController.pushViewController(quotesViewController, animated: true)
}

Build and run the application to make sure we didn't break anything.

Conforming SettingsViewController to Storyboardable

The QuotesViewController class instantiates the settings view controller in its settings(_:) method. Let's simplify the implementation by conforming the SettingsViewController class to the Storyboardable protocol. Open SettingsViewController.swift and conform the SettingsViewController class to the Storyboardable protocol.

import UIKit

class SettingsViewController: UIViewController, Storyboardable {

    ...

}

The SettingsViewController class defines a static, computed property with name storyboardIdentifier. We can remove the static, computed property since the protocol extension already provides a default implementation.

Open QuotesViewController.swift and navigate to the settings(_:) method. We can instantiate a SettingsViewController instance by calling the instantiate() method on the SettingsViewController class.

@IBAction func settings(_ sender: Any) {
    // Initialize Settings View Controller
    let settingsViewController = SettingsViewController.instantiate()

    // Present Settings View Controller
    present(settingsViewController, animated: true)
}

Build and run the application and tap the Settings button in the top right to make sure we didn't break anything.

Adding Unit Tests

Another benefit of the solution we implemented is increased testability. Instantiating a view controller should never fail and that's something we can guard against by writing a few unit tests. Add a test target to the project by choosing New > Target... from Xcode's File menu. Choose the iOS Unit Testing Bundle template from the iOS > Test section.

Adding a Test Target

The default configuration of the test target is fine.

Adding a Test Target

Expand the QuotesTests group and rename QuotesTests.swift to StoryboardableTests.swift. Rename the QuotesTests class to StoryboardableTests and remove the current implementation of the StoryboardableTests class.

import XCTest

class StoryboardableTests: XCTestCase {

}

To gain access to the internal entities of the Quotes module we need to add an import statement for the Quotes module and prefix the import statement with the testable attribute.

import XCTest
@testable import Quotes

class StoryboardableTests: XCTestCase {

}

The first unit test tests the instantiation of a QuotesViewController instance. Create a method with name testQuotesViewController().

func testQuotesViewController() {

}

The unit test is very easy to implement. We call the instantiate() method on the QuotesViewController class. That's it. The XCTest framework doesn't provide the ability to assert whether an expression throws a fatal error. We could override the fatalError(_:file:line:) function to work around this limitation, but isn't an appealing solution. We're automatically notified if one of the unit tests fails because the test suite is interrupted if a fatal error is thrown. That's fine for now.

func testQuotesViewController() {
    _ = QuotesViewController.instantiate()
}

Choose Test from Xcode's Product menu or press Command + U to run the test suite. The unit test should pass without issues.

The unit test for the SettingsViewController class is almost identical. Create a method with name testSettingsViewController(). In the unit test, we call the instantiate() method on the SettingsViewController class.

func testSettingsViewController() {
    _ = SettingsViewController.instantiate()
}

Run the test suite one more time to make sure the unit tests pass.

What's Next?

Swift protocols and protocol extensions are quite powerful. This episode has illustrated how to use a protocol and a protocol extension to simplify a project without adding a lot of complexity. We were able to rid the project of a few string literals and made it easier and more elegant to instantiate view controllers from a storyboard.

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy
Next Episode "Navigating With a Coordinator"