Unit Testing Nibs in Swift — Part 2

Joe Susnick
May 7, 2017 · 7 min read

I hope you had a chance to read the first part — Unit Testing Nibs in Swift — if not please read and 💚 it. This project picks up where that one leaves off. Or, if you’d rather check out the starter project on my github: https://github.com/joesus/TDDNibs.

Open and run the project, also the tests — everything should run and pass.

Your app should look like this:

Pretty, but useless

We need to make this a little more useful. Lets experiment with adding a label and hooking it up.

Time to write a test!

In TestingNibsTests.swift add the following test:

func testInitializingWithFrameSetsLabel() {
let frame = CGRect(x: 0, y: 0, width: 10, height: 10)

guard let _ = CustomView(frame: frame).label else {
return XCTFail("CustomView should have a label when instantiated from code")
}
}

This checks to see if CustomView is initialized with a label property.

Run your tests!

It will not compile because we’re missing a variable for the label property.

Value of type ‘CustomView’ has no member ‘label’

Make it green!

First thing to do is get your test to compile. Add the following to CustomView.swift

@IBOutlet weak var label: UILabel!

Run your tests again!

They fail with the error we wrote. Excellent!

CustomView should have a label when instantiated from code

Make it green! (again)

Open CustomView.xib and drag a UILabel onto the view. Give it vertical and horizontal constraints.

Center it reeeal good

Now you may be wondering how to hook this up.

Go to File’s Owner and notice that there is no class set.

Huh?

Up until now we didn’t have to worry about this at all. The file didn’t need an owner because there was nothing to own. The loadNib() method of CustomView only knows the name of the file whose view it’s loading, nothing more. The names CustomView.swift and CustomView.xib are the same but that’s just a strategy to keep us organized. There is no magic.

For example: Potato.swift can load Banana.xib but it makes more sense to have Banana.swift load Banana.xib. People who follow naming conventions are far less likely to go insane.

Anyway, we now have a property (our label) and something needs to own a reference to it.

Change the class from NSObject to CustomView and notice that the label is now available in the list of outlets. Drag to connect it to the label we added earlier.

There it is!

Run your tests again!

Success! You now have a label property that’s connected to your nib!


Time to write another test!

Under testInitializingWithCoder() add the following test:

func testInitializingWithCoderSetsLabel() {
let customView = CustomView()
guard let label = customView.label else {
return XCTFail("CustomView should load from storyboard with label")
}
XCTAssertEqual(label.text, "Loaded From Nib",
"Label on CustomView should load with text set from the storyboard")
}
  • We instantiate customView with the default init() which will call init(coder:).
  • We check that the label property was set.
  • We check that the text on the label is the text we set from the nib.

Run your tests again!

They’ll fail with the message: ‘Label’ is not equal to ‘Loaded From Nib’.

(“Optional(“Label”)”) is not equal to (“Optional(“Loaded From Nib”)”) — Label on CustomView should load with text set from the storyboard

Make it green! (again)

In CustomView.xib, open the attributes inspector and change the text on Label from ‘Label’ to ‘Loaded From Nib’

New text time!

Run your tests again!

It passes! This is the most basic way of hooking up an outlet from code to nib. Your class knows about the label and you can change the label’s text from either place. Now, let’s get weird.


Time to write another test!

What if we wanted our label to say “Loaded From Nib” in one controller and “Bananas” in a different controller?

We could make two labels and hide one depending on where we are but that’s a lot of work and most of us programmers are lazy. Also we’d have to write a bunch of tests to check when things are hidden or not and that’s annoying too.

So we’ll write a test describing what we want to do. Then we’ll figure out how to do it. This is what people mean when they talk about the joys of testing. We get to imagine what we want. The sky’s our oyster!

In ViewControllerTests.swift add the following test:

func testViewControllerCanSetCustomViewLabel() {
guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? ViewController else {
XCTFail("Could not instantiate vc from Main storyboard")
return
}
vc.loadViewIfNeeded() guard let customView = vc.customView else {
XCTFail("ViewController should have outlet set for customView")
return
}
guard let label = customView.label else {
XCTFail("CustomView should load from storyboard with label")
return
}
XCTAssertEqual(label.text, "Bananas",
"CustomView label should be attached to Main storyboard")
}

This is basically the same test as the one above it.

Run your tests again!

It fails, of course.

(“Optional(“Loaded From Nib”)”) is not equal to (“Optional(“Bananas”)”) — CustomView label should be settable from ViewController

Make it green! (again)

In ViewController.swift modify your IBOutlet to look like this:

@IBOutlet weak var customView: CustomView! {
didSet {
customView.label.text = "Bananas"
}
}

You could also set the label text in viewDidLoad() but that opens the possibility of trying to set a property on an outlet that is not instantiated which would result in a crash. Remember: ViewController has no guarantee that customView has it’s outlets wired up correctly.

Run your tests again!

Green!

Run the app just for fun… The label has changed. Magic!

🎤B-A-N-A-N-A-S🎤

Now, I promised we’d get weird so let’s get weird.

Open Main.storyboard and you should see something like this:

Add another label anywhere on the storyboard except inside of customView. Attach outlet from the customView to the new label.

Run the app. It should look like this:

This looks stupid

Run your tests again!

It fails with

This is confusing but is important. There are two sets of mappings going on in a specific order:

  • The test calls loadViewIfNeeded() on the test vc
  • Main.storyboard is tasked with decoding objects and mapping them to ViewController
  • First, Main.storyboard tries to map a CustomView to the customView property on ViewController but it doesn’t have that view available.
  • The absence of a CustomView during the mapping from Main.storyboard to ViewController triggers a second mapping from CustomView.xib to CustomView.
  • CustomView.xib sets the label property on CustomView as part of this second mapping; the mapping from CustomView.xib to CustomView. The text is set to “Loaded From Nib” as it is in CustomView.xib
  • Once the second mapping is complete and CustomView is available, the first mapping can continue. ViewController sets the customView property to the value made available from the (now completed) second mapping. It changes the label of it’s CustomView's text to “Bananas”
  • Now, the first mapping continues and the next object it encounters is a UILabel. This is a different label from the one that set during the second mapping — the one from CustomView.xib to CustomView.
  • The new UILabel is set to the label property of the CustomView. The text for the new label is “Another Label” as it is in Main.storyboard. Which makes sense because of this crappy picture.
A crappy picture. I hope you get the point.

Either way the test fails.

So you can wire up the label outlet on customView to a different label on ViewController, but this is not a sane thing to do. You should not do this. But, I think it’s interesting that you can do it. Also I hope it gives you a better idea of how object mapping works. If you can think of a good reason to do this; I want to hear from you.

Delete the changes so your tests pass again.

Back to green. Whew.


Takeaways:

  • You need to have a class as the File Owner in order to set outlets
  • Your nib file and class file have the same name but are not automagically associated
  • You can override a property on a custom view in the didSet of the customView property where the custom view is being used
  • You can wire up outlets from a view loaded from a nib to elements located outside of the nib again illustrating that there is no automagic link between a file and a nib
  • The order in which objects are unmapped from a storyboard

The complete code for the project is at https://github.com/joesus/TDDNibs, checkout the branch: “part-2”

Hope you got something out of this. I’m going to keep writing them because they help me learn. Thanks for reading!

More From Medium

Related reads

Also tagged Unit Testing

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade