UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Debugging Auto Layout feedback loops

How to use UIViewLayoutFeedbackLoopDebuggingThreshold today

Paul Hudson       @twostraws

  • This article is dedicated to Marian Goldeen and Russell Ladd, who work tirelessly to help all of us build great apps.

A while 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() {
    view.setNeedsLayout()
}

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!

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.