Question

I'm currently having a hard time understanding why the following unit test fails on an iPad 2. Auto layout seems to slightly (by 0.5 points) mis-position view inside superview relative to the exact centering that's required by two layout constraints. What seems especially strange is that the crucial test (but-last assertion) passes on an iPhone 5, so the apparent rounding error affects only one (iOS 6) platform. What's going on here?

UPDATE 1 I've changed the code to ensure that that both frames are sufficiently constrained in terms of widths and heights even if translatesAutoresizingMaskIntoConstraints is NO, as suggested as a possibly related remedy here. However, this apparently does not change the situation.

#import "BugTests.h"

@implementation BugTests

- (void)testCenteredLayout {
    UIView *superview = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 768, 88)];
    superview.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
    superview.translatesAutoresizingMaskIntoConstraints = YES;

    UILabel *view = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
    view.text = @"Single Round against iPad.";
    view.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
    view.translatesAutoresizingMaskIntoConstraints = NO;
    [view addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeWidth  relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:206.0]];
    [view addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant: 21.0]];

    [superview addSubview:view];

    [superview addConstraint:[NSLayoutConstraint constraintWithItem:superview attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:superview attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    STAssertEquals(superview.center, CGPointMake(384, 44), nil); // succeeds
    STAssertEquals(view.center,      CGPointMake(  0,  0), nil); // succeeds

    [superview setNeedsLayout];
    [superview layoutIfNeeded];

    STAssertTrue(!superview.hasAmbiguousLayout, nil);

    STAssertEquals(superview.frame.size, CGSizeMake(768, 88), nil); // succeeds
    STAssertEquals(view.frame.size,      CGSizeMake(206, 21), nil); // succeeds

    STAssertEquals(superview.center, CGPointMake(384, 44), nil); // succeeds

    STAssertEquals(superview.center, view.center,            nil); // fails: why?
    STAssertEquals(view.center,      CGPointMake(384, 44.5), nil); // succeeds: why?
}

@end

UPDATE 2 I've isolated another instance of (apparently) the same problem in a second unit test. This time it involves a top (not center) constraint, and this time a fractional point coordinate seems to be the trigger. (The test succeeds also on pre-Retina devices e.g. with y = 951, i.e. an odd point coordinate.) I've checked in various simulator configurations (next to my physical iPad 2 and iPhone 5) occurence indeed seems tied to the absence of a Ratina display. (Again, thanks to @ArkadiuszHolko for the lead.)

My current sense from these tests is that one has to avoid odd heights and fractional y-coordinates if point-exact auto layout on pre-Retina displays is required. But why?

- (void)testNonRetinaAutoLayoutProblem2 {
    UIView *superview = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 768, 1004)];
    superview.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
    superview.translatesAutoresizingMaskIntoConstraints = YES;

    CGFloat y = 950.5; // see e.g. pageControlTopConstraint

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
    view.translatesAutoresizingMaskIntoConstraints = NO;
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading  relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeLeading        multiplier:1.0 constant:0.0]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeTrailing       multiplier:1.0 constant:0.0]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop      relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeTop            multiplier:1.0 constant:y]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight   relatedBy:NSLayoutRelationEqual toItem:nil       attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:8]];

    [superview addSubview:view];

    [superview setNeedsLayout];
    [superview layoutIfNeeded];

    STAssertTrue(!superview.hasAmbiguousLayout, nil);
    STAssertTrue(!view.hasAmbiguousLayout,      nil);

    STAssertEquals(superview.frame, CGRectMake(0, 0,       768, 1004), nil); // succeeds
    STAssertEquals(view.frame,      CGRectMake(0, y,       768,    8), nil); // fails: why?
    STAssertEquals(view.frame,      CGRectMake(0, y + 0.5, 768,    8), nil); // succeeds: why?
}
Was it helpful?

Solution

What you've shown is that autolayout abhors misaligned views. On non-retina devices the closest pixel is the nearest point, so it rounds to whole numbers. On retina screens the closest pixel is the nearest half point, so it rounds to the nearest .5. You can demonstrate this by changing y in your second test to 950.25 and noting that view.frame remains {{0, 950.5}, {768, 8}} (instead of changing to {{0, 950.25}, {768, 8}}).

(Just to prove that it is rounding and not ceiling, if you change y to 950.2 view.frame becomes {{0, 950}, {768, 8}}.)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top