IBOutlet Local Reasoning: What, why, and how

Did you watch Protocol and Value Oriented Programming in UIKit Apps from WWDC 2016 yet? I did and it’s one of my favorite sessions yet. It was essentially a part 2 from the WWDC 2015 session, Protocol-Oriented Programming in Swift. It even included a cameo of Crusty. I really liked the session because it gives some plain talk explanation towards improving your Swift code through basic functional programming techniques, and it was specifically attractive to me because of this year’s session’s focus on UI code. UIKit code is filled with object-oriented techniques and patterns like MVC and subclassing that it’s hard for a chiseled object-oriented veteran to learn new tricks like functional programming.

State is Bad

Relying on instance variables in your objects leads to buggy code. Furthermore, it leads to code that’s not just buggy, but also simply hard to debug. You should strongly consider any case where you introduce an instance variable that will be used for state management. Consider the following view controller:

class ViewController: UIViewController {

  var total = 0

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    if (total > 0) {
      print("total bigger than 0")
    }
  }

  func incrementTotal() {
    total = total + 1
  }
}

This code has an outrageous example of relying on an instance variable to determine state of the object. total is an Int. The major problem I have with code like this is the conditional if-statement in viewDidAppear(_:). This is a simple example, so it may not seem problematic to you. Imagine if the view controller had significantly more code, where total was referenced all over the code. Imagine there were numerous places where total was getting assigned to. Imagine there were numerous places where total was being used for conditional checks. Imagine that even external classes were reading and modifying total. Imagine all the places you would have to check and be aware of to ensure that total behaved correctly such that the view controller properly functioned. It quickly grows out of control. Code like this has been the source of more bugs than I can count in my UIKit programming. It’s why functional programming is so attractive to me, despite how hard it is for my head to wrap around some of the concepts.

Local Reasoning

One of the absolute most attractive things about functional programming in my mind is the lack of “side effects” of your code. Essentially, you strive to write code that does not have side effects, where there is a single “source of truth.” This is one of the cores of value oriented programming. When working towards this, you begin to write code that is much easier to debug and much less buggy to begin with. It’s much easier to understand code just by looking at it. One of the biggest anti-patterns of object oriented code is managing state of an object through a instance variable.

In the WWDC 2016 session Protocol and Value Oriented Programming in UIKit Apps the concept of “Local Reasoning” is introduced. It’s a semi-official computer science term that describes code which contains little to no side effects. Local reasoning is that “when you look at the code right in front of you, you don’t have to look at the rest of the code or project to know how it behaves.” This is really attractive because when you look at a piece of code, you can quickly have confidence that you understand what it does because you don’t need to look in many places. Instance variables that control object state are the enemy of local reasoning. For example, looking at the code above, any reference to total contains the caveat that there could be any number of things modifying total throughout program execution. You can never rely on the value of total to actually make a decision about the state of the program.

IBOutlet Local Reasoning

Of course you can’t actually remote all instance variables from your code. You’ll inevitably find certain scenarios where some state must be stored. One common such example are IBOutlets. IBOutlets are special snowflakes in the software design world. Pretty much any rule about software design may not apply when using IBOutlets. When using a Interface Builder file or a Storyboard, you’ll need to subclass UIView or UIViewController and link user interface elements from Interface Builder to the code through IBOutlets. The thing is, IBOutlets take the form of instance variables in code.

class ViewController: UIViewController {
  @IBOutlet weak var toggleSwitch: UISwitch!
}

So how can you apply IBOutlet local reasoning to improve your code? My coworker Mike Stanziano had a really useful idea on how to do this: use a property observer on the IBOutlet.

Let’s say that you want to create a UISwitch in your storyboard. The state of this switch will correspond to a boolean value in a persistence layer, the example will use UserDefaults. Initializing the value usually would happen in viewDidLoad() like:

override func viewDidLoad() {
  super.viewDidLoad()
  let keep = UserDefaults.standard().bool(forKey: "ToggleValue") ?? false
  toggleSwitch!.setOn(keep, animated: false)
}

Maybe you could use the Extract Method refactoring to move those two lines to their own method, but my point is that this code immediately violates local reasoning and starts you off on the wrong foot in using your IBOutlet. I’d consider this code part of initialization of the UISwitch, so it doesn’t make sense to put it in viewDidLoad() aside from the semi-artificial rule put in place by UIKit in that views should be setup in viewDidLoad().

Inspired by Protocol and Value Oriented Programming in UIKit Apps, my colleague Mike came up with something better:

@IBOutlet weak var toggleSwitch: UISwitch! {
  didSet {
    let keep = UserDefaults.standard().bool(forKey: "ToggleValue") ?? false
    toggleSwitch!.setOn(keep, animated: false)
  }
}

This code moves the initialization of the UISwitch‘s state out of viewDidLoad() to a place that I argue is much more appropriate, where the IBOutlet is defined. Since IBOutlets don’t get init’d in code, their definition is the closest thing we have towards an actual instantiation. Now, when you look at the definition of the IBOutlet, you immediately know how it’s initial state is configured. This is right in line with the local reasoning – “when you look at the code right in front of you, you don’t have to look at the rest of the code to know how it behaves.”

And of course, writing a unit test for this code is extremely straightforward.

func testSwitch_DisplaysStoredSwitchState_WhenToggled() {
  // Start the switch on
  UserDefaults.standard().set(true, forKey:"ToggleValue")
  UserDefaults.standard().synchronize()

  let toTest = ViewController()
  let testSwitch = UISwitch()
  toTest.toggleSwitch = testSwitch

  XCTAssertTrue(toTest.toggleSwitch.isOn)
}

No need to call viewDidLoad() from your test. Simply set the desired value on the UISwitch and verify the behavior.

Clean Code is Clear Code

I love the principle of local reasoning in your code. I’m all about writing clean code, I even named this blog after it. Give IBOutlet local reasoning a try and I think you’ll like it. Clean code is enabled by clear code, and local reasoning leads you to clearer code. Give it a try. Here’s a project on GitHub with this code all wired up using Swift 3 and Xcode 8.

Happy cleaning.

Storyboard Code Review Tutorial

It’s extremely important to keep your storyboards and xibs in tip top shape. Just like code, one of the easiest ways to do this is through storyboard code review. I heard the question posed on the iOhYes podcast, episode #108, “What’s involved with storyboard code review?” I’m proud of the team that I work on, and I think we’ve come up with a fairly mature process of storyboard code review. Here’s what we look for:

Most Important Thing – Small Commits

The first thing I look for, is the commit small enough such that it adds one concise piece of functionality to the app in an end to end fashion? Or does the commit add multiple things, or even partial implementations? This goes beyond just storyboards, so you can apply this to all aspects of code reviewing a commit. This fundamental building block for structuring your commits to be atomic, small, and concise will facilitate an easier code review. We strive for less than 500 changed lines of code per commit not including storyboard, project files, or added dependencies (yes, we check in our CocoaPods and you should too).

By keeping your commit small, reviewers of the code will not be overwhelmed with what they are looking at. Things will not slip through the cracks. And it will be all the more obvious as to “what’s changed in this commit?”

Happy Auto Layout

Assuming you are using Auto Layout (and if you’re not, you should be), another important thing to look for during storyboard code review is whether Auto Layout is correctly defined in the storyboard. Specifically, Auto Layout should know how to position every item in the storyboard. This is not pedantic, it’s correct. Remember, bugs come from incorrect Auto Layout. You don’t want bugs do you?

Auto Layout constraints should be:

  1. Comprehensive for each UIView – nothing ambiguous
  2. Correct relative to how the UIView is placed – nothing misplaced

There’s two ways to this. First, my preferred way:

Look for either misplaced="YES" or ambiguous="YES" in the storyboard XML itself. I prefer this way because when looking at a Github pull request, it’s easy to just Command-F on the page for those two terms.

An alternate way, open the storyboard in Interface Builder and look through each scene for the red or yellow warning dot that something isn’t right with Auto Layout.

Screen Shot 2016-04-24 at 2.43.26 PM

Runtime Errors

Don’t forget that even if the storyboard itself looks okay, things can go wrong at runtime, especially if you are either programmatically tweaking constraints, or even must manually manipulating frames or views. If you’ve used Auto Layout long enough, you’ve definitely seen an error like this in the Console:

2012-07-26 01:58:18.621 Rolo[33597:11303] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSAutoresizingMaskLayoutConstraint:0x887d630 h=--& v=--& V:[UIButtonLabel:0x886ed80(19)]>",
    "<NSAutoresizingMaskLayoutConstraint:0x887d5f0 h=--& v=--& UIButtonLabel:0x886ed80.midY == + 37.5>",
    "<NSAutoresizingMaskLayoutConstraint:0x887b4b0 h=--& v=--& V:[UIButtonLabel:0x72bb9b0(19)]>",
    "<NSAutoresizingMaskLayoutConstraint:0x887b470 h=--& v=--& UIButtonLabel:0x72bb9b0.midY == - 0.5>",
    "<NSLayoutConstraint:0x72bf860 V:[UILabel:0x72bf7c0(17)]>",
    "<NSLayoutConstraint:0x72c2430 UILabel:0x72bfad0.top == UILabel:0x72bf7c0.top>",
    "<NSLayoutConstraint:0x72c2370 UILabel:0x72c0270.top == UILabel:0x72bfad0.top>",
    "<NSLayoutConstraint:0x72c22b0 V:[UILabel:0x72bf7c0]-(NSSpace(8))-[UIButton:0x886efe0]>",
    "<NSLayoutConstraint:0x72c15b0 V:[UILabel:0x72c0270]-(NSSpace(8))-[UIRoundedRectButton:0x72bbc10]>",
    "<NSLayoutConstraint:0x72c1570 UIRoundedRectButton:0x72bbc10.baseline == UIRoundedRectButton:0x7571170.baseline>",
    "<NSLayoutConstraint:0x72c21f0 UIRoundedRectButton:0x7571170.top == UIButton:0x886efe0.top>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x72bf860 V:[UILabel:0x72bf7c0(17)]>

Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

It’s nearly impossible to manually go through every view in an app with each storyboard code review, but that doesn’t mean you shouldn’t at least run the app and inspect any views that were added or changed with the commit. And the smaller the commit, the more focused this review can be. When going through this manual review of the app, keep an eye on the Console for that error message. It indicates something isn’t quite right with the Auto Layout, and probably needs to be addressed by the author.

Check For Valid IBActions

The last thing any of us want is our apps to crash. That’s even worse than a crappy user interface when it comes to losing a user’s faith in your app. Your app will crash if a UIControl is connected to an IBAction that no longer exists. You should look for this when doing storyboard code review. I’ve had this happen to me so many times, that having my peers look for this has saved my bacon more times than not. Specifically, this happens when:

  1. Connect an IBAction to a UIControl in Interface Builder
  2. Delete the IBAction method from the code.

Interface Builder does not disconnect the IBAction. If the end user triggers this action, the app will crash.

There are a couple ways to look for this: manually check every new or modified IBAction to ensure that the corresponding method exists, or manually test the app to verify than any new or modified control works. You can also do this verification through automated acceptance tests as well.

Open the Storyboard in Interface Builder

As part of storyboard code review, I recommend simply opening the storyboard in Interface Builder. This confirms two things:

  1. The file can be opened. Don’t dismiss this. I often have had manual merges of storyboards go wrong such that the resulting file couldn’t even be opened in Interface Builder. Code shouldn’t get merged like this.
  2. Interface Builder shouldn’t change the storyboard, just by opening it. Sometimes, I’ve observed that if the storyboard is using a custom font, and that font isn’t installed correctly on each machine, that Interface Builder will reposition all UILabels in the storyboard automatically when the file is opened. You can easily tell if this has happened because simply opening the storyboard will cause a local change in the file. This should not happen.

Look For Relevant Tests

This one is a little more subjective, and depends on your team’s agreed upon strategy for tests. On my team, we try to do our best to have redundantly high test coverage for our apps, at both the unit and acceptance test tiers of the testing pyramid. The acceptance tests are a little more loosely covered. During storyboard code review, I’ll check whether any of the changes made have corresponding tests written of the appropriate nature, and where possible make suggestions for better verification and coverage.

Wrap Up

Storyboards are code too. Just because you aren’t writing it by hand, doesn’t mean it shouldn’t be held to the same high standard as Swift or Objective-C code. A user’s opinion of the app can immediately drop if something in the user interface doesn’t look right. That starts in Interface Builder, hold yourself and your team to a higher standard. I’d love to hear how these suggestions for storyboard code review work for you.

Happy cleaning.