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:
myView.widthAnchor.constraint(equalToConstant: 20).isActive = true myView.widthAnchor.constraint(equalToConstant: 40).isActive = true
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:
2019-05-12 14:56:24.668666-0400 ConstraintConflicter[3619:673082] [LayoutConstraints] 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. ( "<NSLayoutConstraint:0x600003a93520 My cool view.width == 20 (active, names: My cool view:0x7fa5fb51c920 )>", "<NSLayoutConstraint:0x600003a93610 My cool view.width == 40 (active, names: My cool view:0x7fa5fb51c920 )>" ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x600003a93610 My cool view.width == 40 (active, names: My cool view:0x7fa5fb51c920 )> Make a symbolic breakpoint at <span style="word-wrap: break-word;" data-mce-style="word-wrap: break-word;">UIViewAlertForUnsatisfiableConstraints</span> to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
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:
( "<NSLayoutConstraint:0x600003a93520 My cool view.width == 20 (active, names: My cool view:0x7fa5fb51c920 )>", "<NSLayoutConstraint:0x600003a93610 My cool view.width == 40 (active, names: My cool view:0x7fa5fb51c920 )>" )
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
$arg2, etc. to refer to the parameters that were passed in to a function.
In the Xcode console, where it says
po is the command for printing an object to the console. Here’s what we get:
(lldb) po $arg1 <NSLayoutConstraint:0x6000008d3340 My cool view.width == 40 (active, names: My cool view:0x7fd1fc901550 )>
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.)
$arg1. Let’s see if the next parameter has what we want:
(lldb) po $arg2 <__NSArrayM 0x6000025a14d0>( <NSLayoutConstraint:0x6000008d2620 My cool view.width == 20 (active, names: My cool view:0x7fd1fc901550 )>, <NSLayoutConstraint:0x6000008d3340 My cool view.width == 40 (active, names: My cool view:0x7fd1fc901550 )> )
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
(lldb) po [$arg2 description] ( "<NSLayoutConstraint:0x6000008d2620 My cool view.width == 20 (active, names: My cool view:0x7fd1fc901550 )>", "<NSLayoutConstraint:0x6000008d3340 My cool view.width == 40 (active, names: My cool view:0x7fd1fc901550 )>" )
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.)
Computers are great at repetitive tasks. Let’s teach ours how to do this one! First, we’ll recap what we know so far:
- We can set a breakpoint for
UIViewAlertForUnsatisfiableConstraintsto halt our program when constraint conflicts are detected.
- While on the breakpoint, we can use
po [$arg2 description]to get the format preferred by WTF Auto Layout.
- 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:
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
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:
#!/usr/bin/env python import sys import subprocess import urllib # Opens an array of constraints in wtfautolayout.com for visualization. # # Usage: set a symbolic breakpoint for UIViewAlertForUnsatisfiableConstraints # and add a Shell Command action that runs this script, # and calls with this parameter: # @(NSString *)[(id)$arg2 description]@ # Argument 0 is the path to the script itself, so we start # counting arguments from 1. Any commas followed by spaces # in the original text are interpreted as argument delimiters, # so we need to join them back together with another comma and space. # They also come in with backslash escapes for characters such as # single and double quotes, so we decode those string escapes as well. input = ", ".join(sys.argv[1:]).decode('string_escape') # The input is an NSString, so it's wrapped in this: @"". # We remove a double-quote from the end and an at-double-quote # from the beginning. stripped_input = input.rstrip('"').lstrip('@"') # Use urllib to URL-encode special characters. quoted_log = urllib.quote(stripped_input) url = 'https://www.wtfautolayout.com/?constraintlog=%s' % quoted_log # Open the URL in the user's default browser. subprocess.call(['open', url])
We must mark this script as executable by running the following Terminal command:
chmod +x /path/to/wtfautolayout.py
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.
That’s it! Now, whenever Xcode encounters an Auto Layout error, WTF Auto Layout will automatically open with a visualization of our layout error.
Don’t want to have to recreate the breakpoint in every project? Right-click it, and choose Move Breakpoint To → User. 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.