Auto Layout: WTF to FTW

 

Auto Layout issues can be hard to debug, but a website called “WTF Auto Layout?” can help. In this post, we’ll learn how to make this site open every time an Auto Layout error is encountered. Along the way, we’ll learn more about how to use Xcode’s debugger and breakpoints. Don’t let the Objective-C code in the middle scare you away; the Automation section at the end gives the short version of how to get this on your own machine! If you’d like, you can follow along in the sample project. The sample app doesn’t actually do anything, but its main view controller sets up some conflicting constraints that we can use to practice our debugging skills.

Conflicting constraints are a common problem when working with Auto Layout, Apple’s system for laying out views on iOS, macOS, and tvOS. Constraints conflict when we have created multiple constraints that cannot be satisfied at the same time, like this:

Here, we are telling Auto Layout to make myView both 20 pt wide and 40 pt wide. The Auto Layout solver can’t satisfy both of these at once, so we get this error printout at run-time:

I’ll leave it as an exercise to the reader to fully digest this message. For now, let’s take a shortcut made possible by WTF Auto Layout, conveniently located at wtfautolayout.com. (The site’s author, John Patrick Morgan, claims that the name stands for “Why The Failure, Auto Layout?”, but we all know what’s really going on here.)

To use the site, we simply copy the part of the log message between (and including) the parentheses:

Then we paste it into WTF Auto Layout. This produces the following output:

WTF Auto Layout even lets us create a permalink if we want to share the output.

The rendered graphical output makes it easier to see which views are involved, and what the conflicts are. Also, note that my view’s name showed up in the output. This is because I set the accessibilityIdentifier on my views. This identifier will appear in layout-related logs, making debugging easier. We can do the same thing with the identifier property on NSLayoutConstraint. (Note that the accessibilityIdentifier is never shown to the user, but it is used to identify views in automated UI testing, so it should be set to meaningful and unique values for each view.)

Turning Up the WTF

WTF Auto Layout is a useful debugging tool, but it’s annoying to have to copy the output log and paste it into the site. Even worse, if we’re not watching the log closely, we might miss the error, because Auto Layout conflicts don’t halt the app. I’ve worked on apps where layout errors were rampant because people didn’t notice and fix them as soon as they came up.

We can do better. Let’s figure out how UIViewAlertForUnsatisfiableConstraints logs its output, and see if we can grab the text between the parentheses and open WTF Auto Layout ourselves.

The original log gives us a clue: Near the bottom, it tells us that we can “Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.” Symbolic breakpoints are different from the breakpoints one sets by clicking on the line number in the Xcode editor. A symbolic breakpoint is set by the name of a symbol, such as a method or function name, and it will trigger whenever that symbol is used at run-time.

To do as the log suggests, go to the Breakpoint navigator in Xcode (Command-8). At the bottom of the window, click the Plus button, and choose Symbolic Breakpoint… from the menu.

In the resulting popup, enter UIViewAlertForUnsatisfiableConstraints in the Symbol field. Autocorrect won’t help us here, so it’s best to copy and paste.

If we run our code again, now Xcode will stop on the new symbolic breakpoint whenever it encounters conflicting constraints.

As we can see from the Debug navigator on the left, we have stopped on stack frame 0, which is where the UIViewAlertForUnsatisfiableConstraints function was called, somewhere deep inside the UIKitCore private framework. Looking back down the stack, we can see that we got here as a result of the layout code in our view controller being called in stack frame 10. But let’s stay in frame 0.

Notice that, when we hit the breakpoint, we don’t get the log output that we got the last time we ran the app. We can conjecture that this is because the UIViewAlertForUnsatisfiableConstraints function has not actually run yet, and maybe it is the function that produces the log output. If that’s the case, maybe we can inspect the parameters that were passed to it, and figure out what information it is using to print the log. In the LLDB debugger, we can use $arg1, $arg2, etc. to refer to the parameters that were passed in to a function.

In the Xcode console, where it says (lldb), type po $arg1. po is the command for printing an object to the console. Here’s what we get:

That looks like the description of a constraint. And if we look at the original log, it turns out that it’s the same constraints that Auto Layout decided to break.

(Nerd alert: The parameters to the function are actually stored in CPU registers, but the specific register names differ from architecture to architecture. LLDB provides $arg1 and friends as convenient aliases for the registers on whatever platform we’re debugging on. Also, note that the memory address from this log doesn’t match up with the earlier one in this post. This is because this is a new run of the app, and memory addresses change from run to run thanks to ASLR.)

That was $arg1. Let’s see if the next parameter has what we want:

That looks close! It’s an array of constraints, and we can make the educated guess that it’s the same array used to print the list of conflicting constraints in the original debug message. But it’s not quite in the right format. There’s that __NSArrayM preamble, and the individual lines are missing the quotation marks from the original.

Let’s think about what is probably happening inside the UIViewAlertForUnsatisfiableConstraints function: it gets passed the breaking constraint and the conflicting constraints, and it prints out a string. We know that UIKit is written in Objective-C, so it’s not going to be using Swift’s string interpolation. That means that it’s probably using something like +[NSString stringWithFormat:], interpolating the parameters into the string using %@. The documentation for the String Format Specifiers tells us that %@ is the specifier for Objective-C objects, and it obtains a string representation by calling -description on the method.

Alright, let’s try calling -description ourselves:

And there it is! If we want, we can copy and paste this text into WTF Auto Layout to confirm that it is formatted correctly.

(Nerd alert: for those new to Objective-C, -methodName is the convention for how to write about an instance method, and +methodName is how to write about a class method.)

Automation

Computers are great at repetitive tasks. Let’s teach ours how to do this one! First, we’ll recap what we know so far:

  1. We can set a breakpoint for UIViewAlertForUnsatisfiableConstraints to halt our program when constraint conflicts are detected.
  2. While on the breakpoint, we can use po [$arg2 description] to get the format preferred by WTF Auto Layout.
  3. We can run scripts in LLDB breakpoints, and pass the output of an LLDB expression as a parameter to the script.

In order to open a browser window with this text in WTF Auto Layout, we need to understand the format of WTF Auto Layout‘s permalink URL structure. Let’s look at the permalink for our example error. We can get this by clicking the Permalink button on the WTF Auto Layout page:

https://www.wtfautolayout.com/?constraintlog=(%0A%20%20%20%20%22%3CNSLayoutConstraint:0x6000008d2620%20My%20cool%20view.width%20==%2020%20%20%20(active,%20names:%20My%20cool%20view:0x7fd1fc901550%20)%3E%22,%0A%20%20%20%20%22%3CNSLayoutConstraint:0x6000008d3340%20My%20cool%20view.width%20==%2040%20%20%20(active,%20names:%20My%20cool%20view:0x7fd1fc901550%20)%3E%22%0A)

It looks like the URL consists of https://www.wtfautolayout.com/?constraintlog=, followed by the URL-escaped output of the log message. (Note that the WTF Auto Layout permalink generator doesn’t seem to consider parentheses to be special characters, but some experimentation reveals that it doesn’t mind if we encode ( and ) as %28 and %29, respectively.)

This means that all we have to do now is to URL-encode the output from LLDB, append it to the URL string, and ask the operating system nicely to open it for us. Python is the scripting language of choice for LLDB, so that’s what I’ve used here. The particulars of the script are commented for anyone interested, but we can also just accept that it works and save it somewhere on our computer as wtfautolayout.py:

We must mark this script as executable by running the following Terminal command:

Next, let’s make our breakpoint run the script. Edit the breakpoint in the Breakpoints navigator by double-clicking the blue chevron next to it, or right-click on the breakpoint and choose Edit Breakpoint…. Click the Add Action button, and then from the Action popup menu, choose Shell Command. This will let the breakpoint call out to an external script.

In the Command field, type or choose the path to the wtfautolayout.py script we saved in the previous step.

In the second text field, which is for passing arguments, enter @(NSString *)[(id)$arg2 description]@. The @ signs bracketing the expression tell LLDB to evaluate what’s between them as an expression. The (NSString *) and (id) casts were what I found I had to use in order to get LLDB to run the expression without errors.

test

That’s it! Now, whenever Xcode encounters an Auto Layout error, WTF Auto Layout will automatically open with a visualization of our layout error.

Bonus Tip

Don’t want to have to recreate the breakpoint in every project? Right-click it, and choose Move Breakpoint ToUser. Now, every Xcode project will have access to this breakpoint. I recommend doing this with all commonly-used debugging breakpoints.

I’m grateful to Harlan Haskins for his help in creating this script. It started with an idle question during the attendees party for Try! Swift NYC 2017, and we spent the next hour wrestling with LLDB and Python in the middle of a bowling alley.

Leave a Reply

Your email address will not be published. Required fields are marked *