Catch UIViewAlertForUnsatisfiableConstraints in production
Asked Answered
S

2

16

Is it possible to catch autolayout constraint ambiguities in production – the equivalent of a UIViewAlertForUnsatisfiableConstraints breakpoint but for production apps?

My goal would be to add a global handler that would report such errors to a logging system.

Stimulus answered 26/8, 2016 at 14:30 Comment(1)
This is a good question. As I understand it, symbolic breakpoints allow you to break on a specific symbol, method, or selector. I've tried externing a global C function UIViewAlertForUnsatisfiableConstraints() and looking to see if that is an instance or class method on UIView but I have found nothing so far.Lapsus
G
8

The symbol UIViewAlertForUnsatisfiableConstraints actually is a function :

_UIViewAlertForUnsatisfiableConstraints(NSLayoutConstraint* unsatisfiableConstraint, NSArray<NSLayoutConstraint*>* allConstraints).

It is private, so you can't replace it.

But it is called from private method -[UIView engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:], which can be swizzled. This method has approximately this content:

void -[UIView engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:] {
  if ([self _isUnsatisfiableConstraintsLoggingSuspended]) {
    [self _recordConstraintBrokenWhileUnsatisfiableConstraintsLoggingSuspended:$arg4]; // add constraint to some pool
  }
  else {
    if (__UIConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints) {
      // print something in os_log
    }
    else {
      _UIViewAlertForUnsatisfiableConstraints($arg4, $arg5);
    }
  }
}

If I understand correctly from this article, __UIConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints will always return NO on iOS, so all you need to do is to check private bool property called _isUnsatisfiableConstraintsLoggingSuspended and call original method then.

This is result code example:

#import <objc/runtime.h>

void SwizzleInstanceMethod(Class classToSwizzle, SEL origSEL, Class myClass, SEL newSEL) {
  Method methodToSwizzle = class_getInstanceMethod(classToSwizzle, origSEL);
  Method myMethod = class_getInstanceMethod(myClass, newSEL);
  class_replaceMethod(classToSwizzle, newSEL, method_getImplementation(methodToSwizzle), method_getTypeEncoding(methodToSwizzle));
  class_replaceMethod(classToSwizzle, origSEL, method_getImplementation(myMethod), method_getTypeEncoding(myMethod));
}

@interface InterceptUnsatisfiableConstraints : NSObject
@end

@implementation InterceptUnsatisfiableConstraints

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    SEL willBreakConstantSel = NSSelectorFromString(@"engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:");
    SwizzleInstanceMethod([UIView class], willBreakConstantSel, [self class], @selector(pr_engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:));
  });
}

- (void)pr_engine:(id)engine willBreakConstraint:(NSLayoutConstraint*)constraint dueToMutuallyExclusiveConstraints:(NSArray<NSLayoutConstraint*>*)layoutConstraints {
  BOOL constrainsLoggingSuspended = [[self valueForKey:@"_isUnsatisfiableConstraintsLoggingSuspended"] boolValue];
  if (!constrainsLoggingSuspended) {
    NSLog(@"_UIViewAlertForUnsatisfiableConstraints would be called on next line, log this event");
  }
  [self pr_engine:engine willBreakConstraint:constraint dueToMutuallyExclusiveConstraints:layoutConstraints];
}

@end

It works on iOS 8.2/9/10 (it doesn't work in iOS 8.1, so be careful), but I can't give any guarantee. Also, it catches constraint problems in system components, such as keyboard/video player/etc. This code is fragile (it can lead to crash on any system version update, parameters change, etc) and I will not recommend to use it in production (guess that it will not pass even automated review process). You have the last word, but you are warned.

However I think that you can use it in builds for internal/external testers to fix bugs in autolayout before production.

Noticed that you are using swift: you can add this code to your swift project with bridging header file.

Gulosity answered 30/8, 2016 at 22:33 Comment(5)
Cool! How did you find the method declaration for _UIViewAlertForUnsatisfiableConstraints?Lapsus
@Lapsus I did a little bit of reverse engineering and spent some time debugging my test project.Gulosity
Can you elaborate on how you reverse engineered UIKit? I'd like to learn more about its internals, and didn't see this C function in any of the private headers. Did you create an app with broken constraints and disassemble the binary?Lapsus
@Lapsus Of course. In short, I use disassembler Hopper to reveal what happens inside target function. It doesn't give you real types of objects (because all objects converted to id at compilation time), but it gives you a number of parameters. Then you could reveal values of parameters in lldb (use $arg1, $arg2, etc). You can learn more about debugging without sources in lldb / Hopper documentation or in related articles.Gulosity
@Lapsus Yep, I created test project that adds broken constraint on button press and added breakpoint to function to reveal stacktrace (usually it gets you some thoughts about the problem).Gulosity
L
0

The short answer is, this is a private API and you shouldn't be messing with it in production code…

…at least not without knowing the related dangers:

A) Apple will reject your app if you try to override SPIs like this in a product submitted to the app store. And if it slips through for some reason, they will catch it at some later date, and that is generally worse.

B) Method swizzling, like @Roman mentions in his answer, often brings with it some likelihood of you destabilizing whatever you're working on further (or in the future). I still worry when I user a third party library that someone is doing something brittle like this under the hood.

With those warnings out there, go ahead, override private methods and swizzle them to your hearts content. Just please don't ship that code.

Lipocaic answered 3/9, 2016 at 3:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.