NEW: Master Swift design patterns with my latest book! >>

Debugging Auto Layout feedback loops

Paul Hudson    March 11th 2018    @twostraws

A few days ago, Tyler Fox mentioned Auto Layout’s feedback loop debugger on Twitter, which was a feature announced back in WWDC 2016 but largely seems to have gone unnoticed since. Apple certainly weren’t trying to hide it – Marian Goldeen spent almost 15 minutes or so talking about it in this video – but with all the other big announcements coming at WWDC it’s easy for things to get lost.

Layout feedback loops don’t happen often, but you’ve almost certainly faced them at some point. They occur when your views are running their layout code, but part way through something triggers them to start their layout pass again. That might be the result of one view changing the size of one of its superviews, or perhaps because you have an ambiguous layout.

Either way, this problem manifests itself as your CPU maxing out and RAM usage steadily marching upwards, all because your views are running their layout code again and again without ever returning. Worse, if you try to debug the problem you’ll just see lots of layout code running – it’s not obvious where the loop is, or what might resolve it.

The Layout Feedback Loop Debugger is designed to solve this problem: you just set one of two launch arguments, depending on whether you want to target iOS/tvOS or macOS, then run your app as normal. Here’s how to try it out:

  1. Go to the Product menu, hold down Option, and choose “Run…”
  2. Select the Arguments tab, and click + to add a new entry
  3. For iOS apps enter -UIViewLayoutFeedbackLoopDebuggingThreshold 100
  4. For macOS apps enter -NSViewLayoutFeedbackLoopDebuggingThreshold 100

Although we’re using 100 above, you can use any number between 50 and 1000 – it represents how many times a view must layout its subviews in a single run loop before it is considered to be a feedback loop. So, by setting 100 we’re saying “allow one view to lay out itself 100 times in a loop before we acknowledge there’s a problem.”

When that limit is reached your app won’t stop immediately. Instead, the layout feedback loop debugger will let layout continue a few more times so it can take a log of exactly what’s happening, before finally raising an exception and printing a gigantic log of its findings. Even the most trivial of feedback loops generates over 500 lines of log data, so brace yourself for some detailed reading!

If you want to try it immediately and see what you get back, just create a simple layout feedback loop in your app. For example, you might add a method like this to a view controller:

override func viewDidLayoutSubviews() {

That will tell its view to redo its layout every time layout continues, so layout will never finish. When you run that code, your app will raise an exception after a split second, but you’ll see about 500 lines of log data in your Xcode console. The feedback loop debugger is extremely verbose, but it does tell you exactly what’s happening – in theory you should be able to leave that launch argument in place, because most of the time it won’t do anything.

Using the code above, part of your log will say this:

2018-03-11 16:11:59.148277+0000 DebugTest[9005:547143] [LayoutLoop] >>>UPSTREAM LAYOUT DIRTYING<<< About to send -setNeedsLayout to layer for <UIView: 0x7fdfb3704040; f={{0, 0}, {375, 812}} > under -viewDidLayoutSubviews for <UIView: 0x7fdfb3704040; f={{0, 0}, {375, 812}} > 
    0   UIKit                               0x0000000104a168e8 -[_UIViewLayoutFeedbackLoopDebugger _recordSetNeedsLayoutToLayerOfView:] + 382
    1   UIKit                               0x00000001041e8407 -[UIView(Hierarchy) setNeedsLayout] + 164
    2   DebugTest                           0x0000000102f7bb5d _T09DebugTest14ViewControllerC21viewDidLayoutSubviewsyyF + 173
    3   DebugTest                           0x0000000102f7bba4 _T09DebugTest14ViewControllerC21viewDidLayoutSubviewsyyFTo + 36
    4   UIKit                               0x00000001041febe9 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1819
    5   QuartzCore                          0x000000010aaed61c -[CALayer layoutSublayers] + 159
    6   QuartzCore                          0x000000010aaf17ad _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 401
    7   QuartzCore                          0x000000010aa7886c _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 364
    8   QuartzCore                          0x000000010aaa5946 _ZN2CA11Transaction6commitEv + 500
    9   UIKit                               0x000000010414826a __34-[UIApplication _firstCommitBlock]_block_invoke_2 + 141
    10  CoreFoundation                      0x00000001074ac05c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    11  CoreFoundation                      0x000000010749083b __CFRunLoopDoBlocks + 203
    12  CoreFoundation                      0x0000000107490014 __CFRunLoopRun + 1300
    13  CoreFoundation                      0x000000010748f889 CFRunLoopRunSpecific + 409
    14  GraphicsServices                    0x0000000109c959c6 GSEventRunModal + 62
    15  UIKit                               0x000000010412d5d6 UIApplicationMain + 159
    16  DebugTest                           0x0000000102f7cda7 main + 55
    17  libdyld.dylib                       0x00000001086a7d81 start + 1

It’s telling you that setNeedsLayout() is about to be called on a view, but it’s also said “>>>UPSTREAM LAYOUT DIRTYING<<<“ because that’s the root of the problem: this isn’t an ambiguous layout problem, but is instead a result of one view forcing part of its view hierarchy to redo layout.

You also get a complete back trace, which contains this call at frame 2: _T09DebugTest14ViewControllerC21viewDidLayoutSubviewsyyF – that’s the viewDidLayoutSubviews() method of our ViewController class, so you can see what’s causing the loop.

If you scroll far enough down the log, you’ll see this output:

Views receiving layout in order: (
    <UIView: 0x7fdfb3704040; frame = (0 0; 375 812); >

This section is designed to help you visualize the order in which layout was calculated. We have a trivial layout here, but for more complex layouts – when you have to scroll a long way through the log! – you’ll see a complete ordered list of how Auto Layout was working.

If you want to try the layout feedback loop debugger in your own apps, here are two last tips:

  1. Because it’s enabled using a launch argument, it will never affect production code shipped through the App Store. So, it has absolutely no performance impact in your finished app.
  2. You may occasionally see the acronym “tAMIC” appear. Marian was kind enough to reveal what that stands for: “translates autoresizing masks into constraints” – obvious in retrospect!

Thanks to Marian for talking about it at WWDC, and to Tyler for nudging our memory!


About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Click here to visit the Hacking with Swift store >>