Load a Storyboard-Based View Controller

We added a second view controller because storyboards usually have more than one of them. How can we access an arbitrary view controller within a storyboard? We’ll do this by assigning a Storyboard ID to the view controller we want.

Create a new test suite named StoryboardBasedViewControllerTests. (Remember to start with Test Zero to confirm that the new suite is hooked up.) Add this import declaration at the top of the file:

LoadViewControllers/LoadViewControllersTests/StoryboardBasedViewControllerTests.swift
 @testable​ ​import​ ​LoadViewControllers

Then add the following test case:

LoadViewControllers/LoadViewControllersTests/StoryboardBasedViewControllerTests.swift
 func​ ​test_loading​() {
 let​ sb = ​UIStoryboard​(name: ​"Main"​, bundle: ​nil​)
 let​ sut = sb.​instantiateViewController​(
  identifier: ​String​(describing: ​StoryboardBasedViewController​.​self​))
 }

The first line loads the Main storyboard. Then the call to instantiateViewController(identifier:) takes an arbitrary identifier. It can be anything as long as it’s unique within the storyboard. Using the class name as the identifier is an easy way to do this.

images/aside-icons/tip.png

When assigning a Storyboard ID to a view controller, use its class name. That way, the identifier will be unique. And when writing tests, you’ll know the ID without having to look it up inside the storyboard.

(There is an edge case: the class name won’t be unique if your storyboard has multiple instances of the same view controller type. Don’t worry—if you use the same Storyboard ID twice, Xcode will tell you.)

Run the tests. The results log will show “1 unexpected failure” with the following message:

 failed: caught "NSInvalidArgumentException", "Storyboard
  (<UIStoryboard: 0x6000032d7bc0>) doesn't contain a view controller with
  identifier 'StoryboardBasedViewController'"

Let’s assign this Storyboard ID to the view controller. Open Main.storyboard, select the “Storyboard Based View Controller,” and look in the Identity Inspector on the right. (If the Identity Inspector isn’t showing, press --4.) In the Identity Inspector, copy and paste the Class name field into the Storyboard ID field, like this:

images/load-view-controllers/storyboard-id.png

Run the tests. This time this test will pass.

But the UIStoryboard method instantiateViewController(identifier:) returns a UIViewController that won’t know about the outlet. We need to downcast this to the actual type of our system under test (or sut). Thankfully, the method returns a generic type. To get the type we want, we can explicitly specify the type of sut:

LoadViewControllers/LoadViewControllersTests/StoryboardBasedViewControllerTests.swift
 let​ sut: ​StoryboardBasedViewController​ = sb.​instantiateViewController​(
  identifier: ​String​(describing: ​StoryboardBasedViewController​.​self​))

(Make sure you’re using the new method which has the argument name identifier, not the old one named withIdentifier.)

If the type is wrong, this will crash the test run—we won’t get any further test case reports. But it will give a useful message in the console log. And in my experience, programmers don’t often introduce typos in the storyboard Class field. Since it’s a rare occurrence, I don’t mind if the tests crash. (But only for storyboards.)

We now have an sut of type StoryboardBasedViewController. Let’s add an assertion to confirm that the outlet is set:

LoadViewControllers/LoadViewControllersTests/StoryboardBasedViewControllerTests.swift
 XCTAssertNotNil​(sut.label)

Run the tests. This will fail, but don’t be disheartened. There’s a trick to making this work: we’ll ask the view controller to loadViewIfNeeded. This will load the view controller’s view from the storyboard, including outlet connections. Here’s a complete test using the simpler force-cast approach:

LoadViewControllers/LoadViewControllersTests/StoryboardBasedViewControllerTests.swift
 func​ ​test_loading​() {
 let​ sb = ​UIStoryboard​(name: ​"Main"​, bundle: ​nil​)
 let​ sut: ​StoryboardBasedViewController​ = sb.​instantiateViewController​(
  identifier: ​String​(describing: ​StoryboardBasedViewController​.​self​))
 
» sut.​loadViewIfNeeded​()
 
 XCTAssertNotNil​(sut.label)
 }

Run the tests to watch this pass. This demonstrates that a unit test can load a specific view controller from a storyboard, with outlets connected.