Почему автоматическая компоновка iOS приводит к очевидным ошибкам округления на дисплеях предварительной сетчатки (включая unit test)

В настоящее время я с трудом понимаю, почему следующее unit test выходит из строя на iPad 2. Автоматическая компоновка кажется слегка (на 0,5 пункта) неправильной позицией view внутри superview относительно точной центровки, которая требуемый двумя ограничениями макета. Что особенно странно, так это то, что решающее испытание (но последнее утверждение) проходит на iPhone 5, поэтому очевидная ошибка округления влияет только на одну (iOS 6) платформу. Что здесь происходит?

ОБНОВЛЕНИЕ 1. Я изменил код, чтобы гарантировать, что оба фрейма достаточно ограничены с точки зрения ширины и высоты, даже если translatesAutoresizingMaskIntoConstraints NO, как это предлагается как возможное средство здесь. Однако это, по-видимому, не меняет ситуацию.

#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

ОБНОВЛЕНИЕ 2 Я выделил еще один экземпляр (по-видимому) той же проблемы во второй unit test. На этот раз это связано с верхним (не центральным) ограничением, и на этот раз координата дробной точки кажется триггером. (Тест преуспевает также на устройствах предварительной сетчатки, например, с помощью y = 951, то есть координаты нечетной точки.) Я проверил различные конфигурации симулятора (рядом с моим физическим IPad 2 и iPhone 5), действительно, кажется, связано с отсутствием дисплей Ratina. (Опять же, спасибо @ArkadiuszHolko за лидерство.)

Мой нынешний смысл этих тестов заключается в том, что нужно избегать нечетных высот и дробных y-координат, если требуется точная автоматическая компоновка на дисплеях предварительной сетчатки. Но почему?

- (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?
}

Ответ 1

То, что вы показали, заключается в том, что автозапуск не соответствует неверным представлениям. На устройствах без сетчатки самый близкий пиксель является ближайшей точкой, поэтому он округляется до целых чисел. На экранах сетчатки ближайший пиксель является ближайшей половинной точкой, поэтому округляется до ближайшего .5. Вы можете продемонстрировать это, изменив y во втором тесте на 950.25 и отметив, что view.frame остается {{0, 950.5}, {768, 8}} (вместо изменения на {{0, 950.25}, {768, 8} }).

(Просто чтобы доказать, что он округлен, а не ceil ing, если вы меняете y на 950.2, view.frame становится {{0, 950}, {768, 8}}.)