跳转至

13 Common Auto Layout Issues

Auto Layout is great, but it’s not a magic formula for creating bug-free interfaces. With Auto Layout, you’ll occasionally run into problems. For instance, your layout may too few constraints, too many constraints or conflicting constraints. Although the Auto Layout Engine will try to solve most of these problems for you, it’s crucial to understand how to handle them.

Understanding and solving Auto Layout issues

Under the hood, the Auto Layout Engine reads each of your constraints as a formula for calculating the position and size of your views. When the Auto Layout Engine is unable to satisfy all of the constraints, you’ll receive an error.

There are three types of errors you’ll encounter while working with Auto Layout:

  • Logical Errors: These types of errors usually occur when you inadvertently set up incorrect constraints. Maybe you entered incorrect values for the constants, choose the wrong relationship between the elements or didn’t take into account the different orientations and screen sizes.
  • Unsatisfiable Constraints: You’ll get these errors when it’s impossible for the Auto Layout Engine to calculate a solution. A simple example is when you set one view to have a width < 20 and a width = 100. Since both of those constraints cannot be true at the same time, you’ll get an error.
  • Ambiguous Constraints: When the Auto Layout Engine is trying to satisfy multiple constraints, and there’s more than one possible solution, you’ll get this type of error. When this happens, the engine will randomly select a solution, which may produce unexpected results with your UI.

It’s time to see how these issues look in a real project, starting with Unsatisfiable Constraints.

Unsatisfiable Constraints

Open the starter project for this chapter and build and run.

img

Note

Your view may look different because of the random selections of the Auto Layout Engine.

Throughout this chapter, you’ll work with this project. It’s a simple memory game where the user has to repeat the sequence by tapping the corresponding box.

With the project still running, switch to Xcode and open Main.storyboard. This is the main screen for the memory game. You should see a header at the top, but there are some conflicts you need to resolve. This is where the console logs can help!

How to read the logs

Look at the console, and you’ll see some useful information. What you’re seeing is a combination of informative text, constraints in visual format language and even some helpful suggestions:

2019-05-05 04:19:42.439737+0200 DebuggingAutoLayout[4310:346296] [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:0x600002069270 UIView:0x7fad7dc0f2f0.height == 100   (active)>",
    "<NSLayoutConstraint:0x600002069900 UILayoutGuide:0x600003a355e0'UIViewSafeAreaLayoutGuide'.bottom == UIView:0x7fad7dc0f2f0.bottom + 800   (active)>",
    "<NSLayoutConstraint:0x600002069950 V:|-(0)-[UIView:0x7fad7dc0f2f0]   (active, names: '|':UIView:0x7fad7dc0d510 )>",
    "<NSLayoutConstraint:0x600002074910 'UIView-Encapsulated-Layout-Height' UIView:0x7fad7dc0d510.height == 667   (active)>",
    "<NSLayoutConstraint:0x600002069860 'UIViewSafeAreaLayoutGuide-bottom' V:[UILayoutGuide:0x600003a355e0'UIViewSafeAreaLayoutGuide']-(0)-|   (active, names: '|':UIView:0x7fad7dc0d510 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600002069270 UIView:0x7fad7dc0f2f0.height == 100   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

Although your log may look slightly different, the general information is the same:

  1. First, there’s a suggestion that you might have too many constraints. If you see one that you didn’t expect, you might need to remove it.
  2. Next, you’ll see all of the constraints related to the affected view using the Visual Format Language. If you need a refresher on this format, refer to Chapter 4, “Construct Auto Layout with Code.”
  3. "<NSLayoutConstraint:0x600002069270 UIView:0x7fad7dc0f2f0.height == 100 (active)>". The first piece, NSLayoutConstraint:0x600002069270, gives the memory address of the NSLayoutConstraint object for each line. The next part, UIView:0x7fad7dc0f2f0, gives the memory address of the view this constraint is attached to. This memory address may not be meaningful to you, but it can help you know when two constraints refer to the same view if they have the same memory address. The last part, .height == 100, tells you what the constraint is. This one simply sets the height to be equal to 100.
  4. "<NSLayoutConstraint:0x600002069900 UILayoutGuide:0x600003a355e0'UIViewSafeAreaLayoutGuide'.bottom == UIView:0x7fad7dc0f2f0.bottom + 800 (active)>". This constraint sets the bottom of a view — you can see it’s the same one as the view in the first line by the memory address — to 800 points above the bottom of the safe area.
  5. "<NSLayoutConstraint:0x600002069950 V:|-(0)-[UIView:0x7fad7dc0f2f0] (active, names: '|':UIView:0x7fad7dc0d510 )>". In this line, after the memory address of the constraint, you see V:. From the Visual Format Language, you know that this means a vertical constraint. This is followed by |-(0)-[UIView:0x7fad7dc0f2f0]. You know that | refers to a view’s superview, -(0)- means there is 0 vertical distance between the superview and the view, and [UIView:0x7fad7dc0f2f0] tells you which view, again, the same one from the first two lines. So, a space of 0 between the top of a view and its superview means the top of the view is aligned with its superview. The last part of the line, names: '|':UIView:0x7fad7dc0d510, gives you the memory address of the superview.
  6. "<NSLayoutConstraint:0x600002074910 'UIView-Encapsulated-Layout-Height' UIView:0x7fad7dc0d510.height == 667 (active)>". This one refers to the superview height constraint. This is not a constraint that you added; it was added by the system to set the height of the view controller’s root view. You can see from the memory address that it’s the same superview referenced in the line above.
  7. "<NSLayoutConstraint:0x600002069860 'UIViewSafeAreaLayoutGuide-bottom' V:[UILayoutGuide:0x600003a355e0'UIViewSafeAreaLayoutGuide']-(0)-| (active, names: '|':UIView:0x7fad7dc0d510 )>". This one sets the bottom of the safe area layout guide to the bottom of the superview. Just like the previous one, this one is created by the system.
  8. Next, it’s telling you that it will try to fix the problem by removing one constraint for you. In this case: <NSLayoutConstraint:0x600002069270 UIView:0x7fad7dc0f2f0.height == 100 (active)>. However, because it picks a random constraint to remove, you should never rely on this.
  9. Finally, it suggests that you create a symbolic breakpoint, which you’ll do in the next section.

When you put all of this together, you’ll see that the view is set to be:

  • 100 points high
  • at the top of the superview
  • 800 points from the bottom of the superview

This requires the superview to be 900 points high. However, the superview itself is only 667 points high. There’s no way for Auto Layout to satisfy all of the constraints!

Symbolic Breakpoints

When you create a Symbolic Breakpoint at UIViewAlertForUnsatisfiableConstraints, the app will stop running if it finds an unsatisfiable constraint. This is useful because sometimes the app may appear OK, when in fact, it has some issues you need to address.

Press Command-8 to go to the Breakpoint navigator.

img

On the bottom left, click +, and then select Symbolic Breakpoint….

img

When the pop-up appears, enter UIViewAlertForUnsatisfiableConstraints in the Symbol field and press Enter.

img

You’ll now see the new breakpoint in the Breakpoint navigator on the left.

img

Build and run. Notice the app pauses when it encounters this issue.

Using Interface Builder to solve conflicts

Open Main.storyboard. In the document outline, click the red circle with an arrow at the right of the Game Scene, and you’ll see a list of the conflicting constraints.

img

Now, click on the red circle at the right of the conflicting constraints header to get more information.

img

Since Auto Layout can not satisfy all of these constraints, you’ll need to delete one. But how do you know which one to remove?

Well, that’s something that comes from a combination of experience and knowing how the interface needs to look. One way to get a better look at these issues is by selecting the element in the document outline.

Click the < Structure button to go back to the document outline. Then, select the Header view, you’ll see some red lines, which indicates that there are conflicts related to that element.

You can also go to the Size inspector and look at the constraints for that element. For the header, you have height, trailing, leading, bottom and top constraints. If you think about it, it doesn’t make much sense to have height, top and bottom constraints since you only need two of those for the engine to be able to infer the other. If you specify top and height, the engine can calculate the bottom; or if you specify top and bottom, the engine can calculate the height; but when you specify top, bottom and height, you have unsatisfiable constraints.

Remove Safe Area.bottom = Header.bottom + 800. To do this, click the red circle again, and this time select the constraint to be removed and click Delete Constraints.

img

The screen no longer shows red lines. Fantastic, you just got rid of the conflicting constraint! Now, build and run.

img

Well, things are looking better, but some views are still not quite right. Also, if you rotate the simulator (Command-left arrow), you’ll see the app does not manage the rotation well, and a lot of things are out of place.

img

First, the board should stay in the center regardless of the orientation.

Open BoardViewController.swift and go to createBoard(), which is where the logic is that creates the board.

There is one fundamental problem with the way the board is created, all of the boxes are created independently, and there isn’t a container that you can center. Also, it will be nice to use stack views to group the boxes in a better way, as you saw in Chapter 3, “StackViews,” many benefits come with the use of them.

Replace createBoard() with this new one:

private func createBoard() {
  var tagFirstColumn = 0
  let numberOfColumns = 4
  let numberOfRows = 4

  //1
  let boardStackView = UIStackView()
  boardStackView.axis = .vertical
  boardStackView.distribution = .fillEqually
  boardStackView.spacing = 10
  boardStackView.translatesAutoresizingMaskIntoConstraints =
    false

  //2
  containerView.addSubview(boardStackView)
  view.addSubview(containerView)

  //3
  NSLayoutConstraint.activate([
    containerView.topAnchor.constraint(equalTo: 
      boardStackView.topAnchor),
    containerView.leadingAnchor.constraint(equalTo: 
      boardStackView.leadingAnchor),
    containerView.trailingAnchor.constraint(equalTo: 
      boardStackView.trailingAnchor),
    containerView.bottomAnchor.constraint(equalTo: 
      boardStackView.bottomAnchor)
  ])

  for _ in 0..<numberOfRows {
    //4
    let boardRowStackView = UIStackView()
    boardRowStackView.axis = .horizontal
    boardRowStackView.distribution = .equalSpacing
    boardRowStackView.spacing = 10

    //5
    for otherIndex in 0..<numberOfColumns {
      let button =
        createButtonWithTag(otherIndex + tagFirstColumn)
      buttons.append(button)
      boardRowStackView.addArrangedSubview(button)
    }

    //6
    boardStackView.addArrangedSubview(boardRowStackView)

    //7
    tagFirstColumn += numberOfColumns
  }

  //8
  blockBoard()
}

With this new function, you:

  1. Create a boardStackView UIStackView and set its axis, distribution and spacing. You also set its translatesAutoresizingMaskIntoConstraints property to false. This stack view will contain all of the rows, which is why its orientation is vertical. The distribution is set to fillEqually and the spacing to 10. This set up guarantees the rows will have an equal separation of 10 between each other.
  2. Add boardStackView to containerView. This view is centered, so it makes sense to put the board inside of it.
  3. Set top, leading, trailing and bottom constraints to be equal between boardStackView and containerView. Now, the container can grow depending on the board size.
  4. Create a boardRowStackView. These are the rows that form the board. You set the axis to be horizontal and the distribution to be equalSpacing with a spacing of 10 to get some padding between the boxes.
  5. Create a loop to add the boxes to the row.
  6. Add boardRowStackView to boardStackView.
  7. Increment tagFirstColumn so that you start with the right value for the next row.
  8. Call blockBoard() so that users cannot play until they tap the play button.

Build and run.

img

Spoiler alert: This layout is still ambiguous, so you may see something slightly different here. You’ll address that in the next section.

There’s still some work to do, but the board is now properly centered.

Before moving on to the next section, here are some tips to prevent Unsatisfiable Constraints:

  • Remember to set translatesAutoresizingMaskIntoConstraints property to false when creating layouts by code.
  • Try to use priorities wisely, i.e., set priority to be 999, when possible, so the Engine knows which constraint can be broken, if necessary. Not all constraints should be required.
  • Avoid giving views with an intrinsic content size a required content hugging or compression resistance. Unless it’s a special case, let the Layout handle it.

Ambiguous layouts

When the Auto Layout Engine arrives at multiple solutions for a system of constraints, you’ll have what’s known as an ambiguous layout.

Although, Interface Builder can offer you solutions with problematic views during design, what happens when your Auto Layout issues happen during runtime?

Fortunately, there are view debug methods available that can help.

UIView debug methods

UIView comes with four useful methods that you can use for debugging your Auto Layout issues:

  • hasAmbiguousLayout: Returns true if the view frame is ambiguous.
  • exerciseAmbiguityInLayout: Randomly selects a solution to illustrate the ambiguity.
  • constraintsAffectingLayout: Returns an array of the constraints affecting the view on the specified axis.
  • _autolayoutTrace: Returns a string with the information regarding the view hierarchy that contains the view.

Dealing with ambiguous layouts

Go back to the starter project and build and run.

Once the app starts, check the logs. There’s nothing there, but the layout doesn’t look quite right. Press Control-Command-Y to pause the program execution.

Now, type the following in the console:

po [[UIWindow keyWindow] _autolayoutTrace]

Scroll to the top, and you’ll see something like this:

img

Note

The command you entered uses Objective-C syntax. When you stop the debugger like this, it stops in framework code, which is written in Objective-C. If you get to this point by setting a breakpoint inside your Swift code, you can use expression -l objc -O -- [[UIWindow keyWindow] _autolayoutTrace]. The first part of the command expression -l objc -O -- tells LLDB that you want to run Objective-C code. After that, you type the command you want to run.

What you see is the view hierarchy of the app. Notice there are several guides in a stack view with the notation “AMBIGUOUS LAYOUT.” By looking through the hierarchy, you can see that this stack view contains several others, so you can identify it as the boardStackView.

*UIStackView:0x7f8be3405bf0
|   |   |   |   *<_UIOLAGapGuide: 0x6000018f9e00 - "UISV-distributing", layoutFrame = {{0, 50}, {0, 10}}, owningView = <UIStackView: 0x7f8be3405bf0; frame = (0 0; 230 230); layer = <CATransformLayer: 0x600002497940>>>- AMBIGUOUS LAYOUT for _UIOLAGapGuide:0x6000018f9e00'UISV-distributing'.minX{id: 595}, _UIOLAGapGuide:0x6000018f9e00'UISV-distributing'.Width{id: 596}

Notice there are some ambiguities between the horizontal position and the width of the UIOLAGapGuide elements; these are responsible for the space between the views inside the UIStackView.

You want the subviews of all of the stack views to fill the space, so you’ll need to change the distribution for the stack view.

In createBoard(), change boardRowStackView.distribution to .fillEqually.

Now, build and run.

img

Once again, pause the program execution (Control-Command-Y), and type this on the console:

po [[UIWindow keyWindow] _autolayoutTrace]

The view tree is already looking better, but there’s another issue with a label at the bottom.

img

Copy the memory address for the label that has the issue; in this case, it’s 0x7fcd5d609c60; however, that address will be different for you. Once you have it copied, hang on to it because you’ll need it soon.

Visual debugging

To solve the remaining issue, you’ll use the Debug View Hierarchy, which gives a visual way to look at how the layout is structured.

Click Debug View Hierarchy in the debug bar. It doesn’t matter if the app is running or paused; it’ll work in either case.

img

Xcode now displays a new window with the view of the app in the middle. Also, there’s a new toolbar you haven’t seen before. This new interactive 3D model gives you a lot of useful information.

You can use the controls in the toolbar at the bottom to navigate the elements that make up the view.

img

View hierarchy toolbar

So what does this new toolbar do? From left to right:

img

  • First slider: This sets the amount of spacing between layers of views. If you’re in 2D mode, it switches to the 3D mode when you change this.
  • Clipping button: Shows/hides clipped content. This view doesn’t have any clipped content, so it doesn’t do anything obvious.
  • Show constraints button: With this mode on, when you select an object, it shows its constraints in a similar way to how they are displayed in Interface Builder.
  • Adjust view mode: With this option, you can choose between three display modes: content, wireframes or both. It hides one or another depending on your selection.
  • Change canvas background color: Changes the canvas to light or dark, which lets you see the views for each mode.
  • Orient to 3D/2D: Switches between 3D or 2D mode for visualization.
  • Zoom controls: Zooms in or out.
  • Range slider: Filters the range of views shown by depth. This allows you to focus only on the layers of views you’re working with.

Right-click any of the blue boxes, and you’ll see a menu.

img

Here’s what you can do, from top to bottom:

  • Print description: Performs a po command on the selected object and prints the information in the console.
  • Focus on -name of the object-: Filters the display to only show that view’s subviews.
  • Show constraints: Shows constraints just like using the button in the toolbar.
  • Hide views in front/behind: Adjusts the range sliders to hide views above or below this view.

Handling runtime issues

Using the memory address copied from the layout trace, paste it on the filter located at the bottom-left side of the screen.

img

You’ll notice that the view hierarchy changed, and now it’s showing just the parts of the tree related to the view you specified, pretty neat. This comes in handy when you want to see the view causing the trouble, and you only have the memory address.

There’s a purple icon with an exclamation sign. This tells you there’s something wrong with that view. If you put your mouse over this icon, you’ll see a tooltip telling you the reason behind the issue.

img

There’s still one more way to get a list of issues: Press Command-5 to show the Issue navigator. Now, click the Runtime tab.

img

Here, you can see issues that are affecting your layout at runtime. Normally, you’d use this to see build time issues, but for this chapter, you’ll focus only on the runtime issues.

Notice there’s an issue indicating that Horizontal position is ambiguous for UILabel. Click on the message in the Issue navigator, and the affected view, in this case, the label displaying “00:00”, is highlighted. It looks like there’s a missing constraint for the label that shows the timer.

Stop the project. Go to TimerView.swift and locate updateConstraints(). There’s only one constraint for timerLabel: its vertical position. To center it horizontally, you need to add one more constraint.

Add this new constraint immediately before the existing one:

labelConstraints.append(
  timerLabel.centerXAnchor.constraint(
    equalTo: safeAreaLayoutGuide.centerXAnchor))

Build and run.

Perfect, the label is now centered, and there are no runtime issues.

img

Common performance issues

The Auto Layout Engines does a lot of things for you. It acts as a dependency cache manager, making things faster, and Apple makes performance-related improvements nearly every year. Nonetheless, you have to be aware of how it works to avoid the unnecessary use of resources.

In the next chapter, you’ll learn about the Render Loop and the Auto Layout Engine, which is key to understanding how to avoid performance issues with Auto Layout.

Churning

Churning is a common issue and one that can easily fly above any developer’s head. It usually happens when you create multiple constraints — many times without them being necessary.

Go to TimerView.swift and look at updateConstraints():

override func updateConstraints() {
  NSLayoutConstraint.deactivate(labelConstraints)
  labelConstraints.removeAll()

  labelConstraints.append(
    timerLabel.centerXAnchor.constraint(
      equalTo: safeAreaLayoutGuide.centerXAnchor))
  labelConstraints.append(
    timerLabel.centerYAnchor.constraint(
      equalTo: safeAreaLayoutGuide.centerYAnchor))

  NSLayoutConstraint.activate(labelConstraints)
  super.updateConstraints()
}

This code works fine; however, the Render Loop runs 120 times every second, and in this scenario, that means you’re deactivating and activating this set of constraints every time.

Since this is a simple UI, you won’t notice performance issues, but that won’t be the case when you create collection views or any complicated layout.

To fix this problem, change the code to this:

override func updateConstraints() {
  if labelConstraints.isEmpty {
    labelConstraints.append(
      timerLabel.centerXAnchor.constraint(
        equalTo: safeAreaLayoutGuide.centerXAnchor))
    labelConstraints.append(
      timerLabel.centerYAnchor.constraint(
        equalTo: safeAreaLayoutGuide.centerYAnchor))

    NSLayoutConstraint.activate(labelConstraints)
  }
  super.updateConstraints()
}

Here, you check if labelConstraints contains any data. If data exists, that means the constraints were already added, so you don’t need to add them again. You also removed some unnecessary code since you don’t have to deactivate the constraints anymore. With these few changes, your code is much more efficient.

Layout feedback loop

Until now, you haven’t seen the second tab of the app: History.

Build and run, and tap the History tab. Oh no, the app is unresponsive! Don’t worry; you’re about to fix that.

Go to Xcode and press Command-7 to show the Debug navigator.

img

Pay close attention to the CPU and Memory indicators. See that? There’s definitely something wrong with the app.

Open the Scheme editor by clicking on top of the app name on the top left side, like this:

img

Click on Edit Scheme.

Add a new argument on the Arguments Passed on Launch section, and enter:

-UIViewLayoutFeedbackLoopDebuggingThreshold 100

Once you’re done, click Close.

img

Build and run. Tap the History tab, and the app still freezes. But this time, when you go to Xcode, you’ll see a log. Now, the app is properly crashing.

img

This is a rather large log. When dealing with the feedback loop you’ll get this kind of log, and it’s never too easy to find the issue. Reading the logs, you’ll see some mentions to setNeedsLayout getting called and also there’s this message:

DebuggingAutoLayout[10441:75492] [LayoutLoop] >>>UPSTREAM LAYOUT DIRTYING<<< About to send -setNeedsLayout to layer for <UITableView: 0x7fd8ec82ae00; f={{0, 0}, {375, 667}} > under -viewDidLayoutSubviews for <UITableView: 0x7fd8ec82ae00; f={{0, 0}, {375, 667}}

It looks like setNeedsLayout might be causing this issue.

Go to ScoresTableViewController.swift and look for any calls to that function. You’ll see it here:

  override func viewDidLayoutSubviews() {
    view.setNeedsLayout()
  }

By calling setNeedsLayout() in viewDidLayoutSubviews(), the app is creating an infinite loop because it’s telling the Render Loop to schedule another update pass just after the first one is finished. Since that’s not of any use, remove the function completely, then build and run.

You can now see the list of scores. If the list is empty, play until you lose and come back again.

Fantastic, you got rid of the bugs, the app is in great shape, and you now know how to deal with some of the common issues that affect Auto Layout.

Key points

  • There are three main types of Auto Layout issues: ambiguous layouts, unsatisfiable constraints and logic errors.
  • You can solve Auto Layout issues faster using UIView methods, Debug View Hierarchy and Interface Builder.
  • To avoid performance issues, you need to understand how the Render Loop and Auto Layout Engine works.