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 callsetTranslatesAutoresizingMaskIntoConstraints:
with the argumentNO
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 equalleftMargin
whensuperview.centerX
isleftMargin/2
:leftMargin = (leftMargin / 2) × multiplier + constant
- In the fixed right margin case,
self.centerX
will equal0
whensuperview.centerX
isrightMargin/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!