Detecting Broken Constraints in Swift

Detecting Broken Constraints in Swift

Broken constraints are common, but there are good reasons you might wanted to get them sorted out. Even though iOS can sometimes provide a functional UI after breaking a constraint, the additional layout passes that this requires are usually a performance issue. If you care about your users' battery usage, you'll want to make sure your UI is always configured and rendered properly.

In this article, I'd like to show a way you can intercept UIKit's exceptions directly in code (not using breakpoints!) and what you can use an implementation like this for.

How do broken constraint alerts work?

The alert you get in Xcode when a constraint breaks is a really interesting little detail of UIKit. Although the alert describes it as an "exception", UIViewAlertForUnsatisfiableConstraints is actually a global C function:

Alt

I imagine the reason they made that choice is because that would be the easiest way to allow you, a developer outside UIKit, to debug such issues -- if you the name of the method that causes something, you can attach a symbolic lldb breakpoint to it. And that's exactly what the alert suggests you to do.

At the time this article was written, UIKit contains four "exceptions":

  • _UIViewAlertForUnsatisfiableConstraints
  • _UITableViewAlertForForcedLayout
  • _UITableViewAlertForLayoutOutsideViewHierarchy
  • _UITableViewAlertForVisibleCellsAccessDuringUpdate

Although you can easily intercept these issues in a debug build by creating a symbolic breakpoint, I spent quite a while trying to figure out how to do that directly in the code. The reason for that is because I wanted to also be able to detect broken constraints in internal releases so that our beta testers could also find and report these issues.

When it comes to altering framework execution, the answer is clearly method swizzling. Unfortunately, after a long search, I couldn't find any easy way to swizzle a global C function. I did eventually find fishhook, which is an utility created by Facebook that is capable of modifying dynamic loader load commands, but it didn't work for UIKit's content for caching reasons. I don't think this is impossible to do per-se, but it's definitely complicated enough to make me not want to bother with it.

Instead, what we need to is find the closest Obj-C method that calls these methods and swizzle that instead. Luckily, in the case of the constraints, we have a very good candidate right off the bat!

Alt

The engine:willBreakConstraint:dueToMutuallyExclusiveConstraints: private method does nothing but to call the relevant C function, making it a perfect swizzling candidate.

Swizzling in Swift

To swizzle this method, I've decided to create a central "interception" object that would bootstrap the swizzling and listen for any broken constraints in the form of a notification. This is because the method above is inside UIView, so notifications work as an easy way to transmit that information somewhere else.

import Foundation
import UIKit

extension Notification.Name {
    static let willBreakConstraint = Notification.Name(
        rawValue: "NSISEngineWillBreakConstraint"
    )
}

final class ConstraintWarningCatcher {

    func startListening() {
        // Note: Only call this **once**!
        let sel = NSSelectorFromString("engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:")
        let method = class_getInstanceMethod(UIView.self, sel)!
        let impl = method_getImplementation(method)

        let replSel = #selector(UIView.willBreakConstraint(_:_:_:))
        let replMethod = class_getInstanceMethod(UIView.self, replSel)!
        let replImpl = method_getImplementation(replMethod)

        class_replaceMethod(UIView.self, sel, replImpl, method_getTypeEncoding(replImpl))
        class_replaceMethod(UIView.self, replSel, impl, method_getTypeEncoding(impl))

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(didReceiveBrokenConstraintNotification),
            name: .willBreakConstraint,
            object: nil
        )
    }

    @objc func didReceiveBrokenConstraintNotification(notification: NSNotification) {
        guard let constraint = notification.object as? NSLayoutConstraint else {
            return
        }
        // do something with this information
    }
}

extension UIView {
    @objc func willBreakConstraint(_ engine: Any, _ constraint: NSLayoutConstraint, _ conflict: Any) {
        willBreakConstraint(engine, constraint, conflict) // swizzled, will call original impl instead
        NotificationCenter.default.post(
            name: .willBreakConstraint,
            object: constraint
        )
    }
}

From here, you can keep an instance of ConstraintWarningCatcher alive and start the observation. When a constraint breaks, the instance will receive a notification.

let catcher = ConstraintWarningCatcher()
catcher.startListening()

let v = UIView()
v.heightAnchor.constraint(equalToConstant: 50).isActive = true
v.heightAnchor.constraint(equalToConstant: 100).isActive = true
view.addSubview(v) // A constraint will break and the catcher will be notified

An important note to be made is that you should never push code involving private APIs to production. In the event that Apple doesn't reject your app (they probably will), there's nothing guaranteeing that Apple won't change how this API works, which could have immediate and devastating effects in your app. Keep this only in your debug builds.

What can you use this for?

For me, what I find this most useful for is that it allows you to have your own backlog of constraint issues. Instead of relying solely on breakpoints, you could use this interception to send this information somewhere (ex: Firebase). Just remember to never push this to actual production builds!

Unfortunately, I couldn't find a good way to swizzle the other exceptions mentioned in this article. Although they are indeed called from Obj-C methods deeper down, these methods are also handling tons of other things, which makes swizzling them a very dangerous idea. Still, being able to do that to constraints alone is already a very good win in my book.