Translating Autoresizing Masks Into Constraints

 ·  iOS, Development, Tutorials

Auto Layout has been a great improvement to the layout system on iOS. It allows expressing layout rules more explicitly and reduces the need for custom layout logic. I especially appreciate that you can mix Auto Layout with manual layout in cases where setting a view's frame expresses the intended behavior more clearly.

When I started considering a mixed approach to layout, I faced the question of what to do with translatesAutoresizingMaskIntoConstraints. As explained in the Auto Layout Guide,

"If you have a view that does its own custom layout by calling setFrame:, your existing code should work. Just don't call setTranslatesAutoresizingMaskIntoConstraints: with the argument NO on views that you place manually."

This approach seemed clear to me until I read Ole Begemann's excellent post about mixing manual layout with Auto Layout:

If you find yourself in a situation that is difficult to solve with Auto Layout, just don’t use it for that particular view…set translatesAutoresizingMaskIntoConstraints = NO on the view since you will position and size it manually, anyway."

To translate or not to translate? To answer this question, I needed a better understanding of what happens when translatesAutoresizingMaskIntoConstraints is YES.

The docs describe its behavior as follows:

If this value is YES, the view’s superview looks at the view’s autoresizing mask, produces constraints that implement it, and adds those constraints to itself (the superview).

Based on this description, I expected to be able to look in the ‑[UIView constraints] array to find the translated constraints, but I quickly learned that the generated constraints are not added there.

After some careful exploration with the debugger, I determined that the constraints are generated on demand during layout by the private method ‑[UIView(UIConstraintBasedLayout) _constraintsEquivalentToAutoresizingMask].

This was an important first step to understanding translatesAutoresizingMaskIntoConstraints. When it is set to YES, the layout system adds the generated constraints to the set of constraints as it solves the layout. When it is set to NO, these constraints are not generated. In this case, if there are other constraints that involve that view they must be sufficient to define its size and position, or if there are no constraints that involve that view, its size and position will stay the same. This means that if you're setting the frame manually, either approach to setting translatesAutoresizingMaskIntoConstraints will work. Turning off translation results in less work for the constraint solver, but whether the CPU savings involved are significant is something to determine on a case-by-case basis.

I could have stopped here. Perhaps, had I listened to the voice of reason, I would have stopped here. Instead I was enticed by a suggestion from Two Toasters' self-proclaimed handsomest developer that it might be informative to reimplement ‑_constraintsEquivalentToAutoresizingMask. Very well…

Let's Build Autoresizing Mask Translation

The method for my reimplementation is named ‑twt_constraintsEquivalentToAutoresizingMask. I also declared ‑_constraintsEquivalentToAutoresizingMask in a category on UIView so that I could use it to check my solution. Here's what the test setup looks like:

@interface UIView (TWTTesting)

- (NSArray *)_constraintsEquivalentToAutoresizingMask;

@end


@implementation TWTViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(160, 120, 80, 80)];
    [self.view addSubview:testView];

    for (UIViewAutoresizing mask = 0; mask < 8; mask++) {
        testView.autoresizingMask = mask;
        NSLog(@"view: %@", testView);
        NSLog(@"constraints: %@", [testView _constraintsEquivalentToAutoresizingMask]);
        NSLog(@"twt_constraints: %@", [testView twt_constraintsEquivalentToAutoresizingMask]);
    }
}

@end

The test iterates from 0 (0b000) to 8 (0b111) to cover all possible combinations of the horizontal bits of the autoresizing mask:

UIViewAutoresizingNone                 = 0,
UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
UIViewAutoresizingFlexibleWidth        = 1 << 1,
UIViewAutoresizingFlexibleRightMargin  = 1 << 2,

To keep things simple, I only implemented translation for the horizontal autoresizing mask bits since the vertical cases would apply the same logic.

At each iteration, the view's autoresizingMask is set, and then the constraints generated by Apple's translation method and those generated by my reimplementation are logged to the console for manual comparison.

Here are the constraints Apple's implementation generates:

testView.frame: (160 120; 80 80)
testView.superview.bounds: (0 0; 320 480)
Reimplementation constraints are instances of NSLayoutConstraint.
Apple's constraints are instances of NSAutoresizingMaskLayoutConstraint.

Autoresizing Mask: UIViewAutoresizingNone
Constraints:
    H:[testView(80)]
    testView.centerX == + 200

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin
Constraints:
    H:[testView(80)]
    testView.centerX == superview.width - 120

Autoresizing Mask: UIViewAutoresizingFlexibleWidth
Constraints:
    testView.width == superview.width - 240
    testView.centerX == superview.centerX + 40

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleWidth
Constraints:
    testView.width == 0.333333 * superview.width - 26.6667
    testView.centerX == 1.66667 * superview.centerX - 66.6667

Autoresizing Mask: UIViewAutoresizingFlexibleRightMargin
Constraints:
    H:[testView(80)]
    testView.centerX == + 200

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin
Constraints:
    H:[testView(80)]
    testView.centerX == 0.625 * superview.width

Autoresizing Mask: UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin
Constraints:
    testView.width == 0.5 * superview.width - 80
    testView.centerX == 0.5 * superview.centerX + 120

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin
Constraints:
    testView.width == 0.25 * superview.width
    testView.centerX == 0.625 * superview.width

In each case, two constraints are generated: one for the view's width and one for its position via centerX. I'll start with the width constraints since there are fewer cases to handle. The easiest is the non-flexible width case. Its constraint is generated like this:

[NSLayoutConstraint constraintWithItem:self
                             attribute:NSLayoutAttributeWidth
                             relatedBy:NSLayoutRelationEqual
                                toItem:nil
                             attribute:NSLayoutAttributeNotAnAttribute
                            multiplier:0
                              constant:CGRectGetWidth(self.frame)];

The flexible width case is a bit more complicated and starts out like this:

[NSLayoutConstraint constraintWithItem:self
                             attribute:NSLayoutAttributeWidth
                             relatedBy:NSLayoutRelationEqual
                                toItem:self.superview
                             attribute:NSLayoutAttributeWidth
                            multiplier:multiplier
                              constant:constant];

The values of multiplier and constant depend on whether the margins are flexible. When both margins are flexible, the width is proportional to the superview's width:

if (flexibleLeftMargin && flexibleRightMargin) {
    multiplier = width / superviewWidth;
    constant = 0;
}

If only one margin is flexible, the width is proportional to the width of the portion of the superview not covered by the fixed margin. The constant is found by observing that the width of the superview that generates a zero-width for self is the width of the fixed margin, which, written as an equation, is 0 = fixedMargin × multiplier + constant. Solving for constant yields the expression we need:

else if (flexibleLeftMargin) {
    multiplier = width / (superviewWidth - rightMargin);
    constant = 0 - rightMargin * multiplier;
}
else if (flexibleRightMargin) {
    multiplier = width / (superviewWidth - leftMargin);
    constant = 0 - leftMargin * multiplier;
}

Finally, if neither margin is flexible, the width is just a fixed difference from the superview's width:

else {
    multiplier = 1;
    constant = width - superviewWidth;
}

The centerX constraints all start out like this:

[NSLayoutConstraint constraintWithItem:self
                             attribute:NSLayoutAttributeCenterX
                             relatedBy:NSLayoutRelationEqual
                                toItem:self.superview
                             attribute:toItemAttribute
                            multiplier:multiplier
                              constant:constant];

The definitions of toItemAttribute, multiplier, and constant break down into six cases. The first one is for a flexible left and right margin and works whether the width is flexible or not:

if (flexibleLeftMargin && flexibleRightMargin) {
    toItemAttribute = NSLayoutAttributeRight;
    multiplier = CGRectGetMidX(self.frame) / superviewWidth;
    constant = 0;
}

This just keeps self.centerX a consistent percent of the way across its superview.

If you look carefully, you'll notice that this is actually different from the constraint that Apple generates. Apple's translation generates a constraint that relates self.centerX to self.superview.width, but if you try to generate a constraint like that, NSLayoutConstraint raises an exception:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** 
+[NSLayoutConstraint constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:]: 
Invalid pairing of layout attributes'

The workaround is to relate self.centerX to self.superview.right, which produces the same effect in cases where the superview.bounds.origin is (0, 0).

Next up are the two cases with one flexible margin and a flexible width:

else if (flexibleLeftMargin && flexibleWidth) {
    toItemAttribute = NSLayoutAttributeCenterX;
    multiplier = CGRectGetMidX(self.frame) / (0.5 * (superviewWidth - rightMargin));
    constant = - (rightMargin / 2) * multiplier;
}
else if (flexibleWidth && flexibleRightMargin) {
    toItemAttribute = NSLayoutAttributeCenterX;
    multiplier = CGRectGetMidX(self.frame) / (0.5 * (superviewWidth - leftMargin));
    constant = leftMargin - (leftMargin / 2) * multiplier;
}

These cases are a little harder. The multiplier is set up to adjust self.centerX in proportion to the change in the center of the region of the superview not covered by the fixed margin. The constant is found from the known cases where the width of that region is zero:

  • In the fixed left margin case, self.centerX will equal leftMargin when superview.centerX is leftMargin/2: leftMargin = (leftMargin / 2) × multiplier + constant
  • In the fixed right margin case, self.centerX will equal 0 when superview.centerX is rightMargin/2: 0 = (rightMargin / 2) × multiplier + constant

The fourth case is for a flexible left margin with everything else fixed:

else if (flexibleLeftMargin) {
    toItemAttribute = NSLayoutAttributeRight;
    multiplier = 1;
    constant = CGRectGetMidX(self.frame) - CGRectGetMaxX(self.superview.frame);
}

In this case, the self.centerX stays a constant distance from the `superview's right edge.

The case for a flexible right margin with everything else fixed is similar:

else if (flexibleRightMargin) {
    toItemAttribute = NSLayoutAttributeLeft;
    multiplier = 1;
    constant = CGRectGetMidX(self.frame);
}

Interestingly, Apple implements this case differently. Apple sets self.centerX equal to a constant, but once again, NSLayoutConstraint raises an exception if you try to do that:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** 
+ [NSLayoutConstraint constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:]: 
A constraint cannot be made that sets a location equal to a constant. Location attributes must be 
specified in pairs'

The final case is for when neither margin is flexible but the width is flexible:

else if (flexibleWidth) {
    toItemAttribute = NSLayoutAttributeCenterX;
    multiplier = 1;
    constant = CGRectGetMidX(self.frame) - CGRectGetMidX(self.superview.bounds);
}

This just keeps self.centerX at a fixed offset from superview.centerX.

That's it! Here are the complete results:

testView.frame: (160 120; 80 80)
testView.superview.bounds: (0 0; 320 480)
Reimplementation constraints are instances of NSLayoutConstraint.
Apple's constraints are instances of NSAutoresizingMaskLayoutConstraint.

Autoresizing Mask: UIViewAutoresizingNone
Apple's constraints:
    H:[testView(80)]
    testView.centerX == + 200
Reimplementation:
    H:[testView(80)]
    testView.centerX == superview.left + 200

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin
Apple's constraints:
    H:[testView(80)]
    testView.centerX == superview.width - 120
Reimplementation:
    H:[testView(80)]
    testView.centerX == superview.right - 120

Autoresizing Mask: UIViewAutoresizingFlexibleWidth
Apple's constraints:
    testView.width == superview.width - 240
    testView.centerX == superview.centerX + 40
Reimplementation:
    testView.width == superview.width - 240
    testView.centerX == superview.centerX + 40

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleWidth
Apple's constraints:
    testView.width == 0.333333 * superview.width - 26.6667
    testView.centerX == 1.66667 * superview.centerX - 66.6667
Reimplementation:
    testView.width == 0.333333 * superview.width - 26.6667
    testView.centerX == 1.66667 * superview.centerX - 66.6667

Autoresizing Mask: UIViewAutoresizingFlexibleRightMargin
Apple's constraints:
    H:[testView(80)]
    testView.centerX == + 200
Reimplementation:
    H:[testView(80)]
    testView.centerX == superview.left + 200

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin
Apple's constraints:
    H:[testView(80)]
    testView.centerX == 0.625 * superview.width
Reimplementation:
    H:[testView(80)]
    testView.centerX == 0.625 * superview.right

Autoresizing Mask: UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin
Apple's constraints:
    testView.width == 0.5 * superview.width - 80
    testView.centerX == 0.5 * superview.centerX + 120
Reimplementation:
    testView.width == 0.5 * superview.width - 80
    testView.centerX == 0.5 * superview.centerX + 120

Autoresizing Mask: UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin
Apple's constraints:
    testView.width == 0.25 * superview.width
    testView.centerX == 0.625 * superview.width
Reimplementation:
    testView.width == 0.25 * superview.width
    testView.centerX == 0.625 * superview.right

This exercise helped me to demystify the process of translating autoresizing masks, and I hope it has been a help to you as well. You can look at the complete implementation of this example on GitHub. If you're looking for a challenge, try extending it to implement the translation for the vertical bits. You can find me on Twitter @a_hershberger. I'd love to hear from you!