Какова связь между UIView setNeedsLayout, layoutIfNeeded и layoutSubviews?

Может ли кто-нибудь дать окончательное объяснение взаимосвязи между методами UIView's setNeedsLayout, layoutIfNeeded и layoutSubviews? И пример реализации, где все три будут использоваться. Спасибо.

Что меня пугает, так это то, что если я отправлю свое собственное представление setNeedsLayout сообщение, то самое следующее, которое он вызывает после того, как этот метод будет layoutSubviews, пропуская прямо над layoutIfNeeded. Из документов я ожидаю, что поток будет setNeedsLayout > вызывает layoutIfNeeded для вызовa > вызывает layoutSubviews.

Ответ 1

Я все еще пытаюсь понять это сам, поэтому возьмите это с некоторым скептицизмом и простите меня, если он содержит ошибки.

setNeedsLayout является простым: он просто устанавливает флаг где-то в UIView, который маркирует его как необходимый макет. Это заставит layoutSubviews вызываться в представлении до того, как произойдет следующее перерисовывание. Обратите внимание, что во многих случаях вам не нужно вызывать это явно из-за свойства autoresizesSubviews. Если этот набор (который он по умолчанию), то любое изменение в кадре представления приведет к тому, что представление выведет его подпрограммы.

layoutSubviews - это метод, в котором вы делаете все интересное. Это эквивалентно drawRect для макета, если хотите. Тривиальный пример может быть:

-(void)layoutSubviews {
    // Child frame is always equal to our bounds inset by 8px
    self.subview1.frame = CGRectInset(self.bounds, 8.0, 8.0);
    // It seems likely that this is incorrect:
    // [self.subview1 layoutSubviews];
    // ... and this is correct:
    [self.subview1 setNeedsLayout];
    // but I don't claim to know definitively.
}

AFAIK layoutIfNeeded обычно не переопределяется в вашем подклассе. Это метод, который вы хотите вызвать, когда хотите, чтобы представление было выложено прямо сейчас. Реализация Apple может выглядеть примерно так:

-(void)layoutIfNeeded {
    if (self._needsLayout) {
        UIView *sv = self.superview;
        if (sv._needsLayout) {
            [sv layoutIfNeeded];
        } else {
            [self layoutSubviews];
        }
    }
}

Вы могли бы называть layoutIfNeeded в представлении, чтобы принудительно вывести его (и его наблюдатели по мере необходимости).

Ответ 2

Я хотел бы добавить ответ n8gray, что в некоторых случаях вам нужно будет позвонить setNeedsLayout, а затем layoutIfNeeded.

Скажем, например, что вы написали пользовательское представление, расширяющее UIView, в котором позиционирование подсмотров является сложным и не может быть выполнено с помощью autoresizingMask или iOS6 AutoLayout. Пользовательское позиционирование может быть выполнено путем переопределения layoutSubviews.

В качестве примера предположим, что у вас есть настраиваемое представление, имеющее свойство contentView и свойство edgeInsets, которое позволяет устанавливать поля вокруг contentView. layoutSubviews будет выглядеть так:

- (void) layoutSubviews {
    self.contentView.frame = CGRectMake(
        self.bounds.origin.x + self.edgeInsets.left,
        self.bounds.origin.y + self.edgeInsets.top,
        self.bounds.size.width - self.edgeInsets.left - self.edgeInsets.right,
        self.bounds.size.height - self.edgeInsets.top - self.edgeInsets.bottom); 
}

Если вы хотите, чтобы анимация смены кадра изменялась при изменении свойства edgeInsets, вам необходимо переопределить установщик edgeInsets следующим образом и вызвать setNeedsLayout, а затем layoutIfNeeded:

- (void) setEdgeInsets:(UIEdgeInsets)edgeInsets {
    _edgeInsets = edgeInsets;
    [self setNeedsLayout]; //Indicates that the view needs to be laid out 
                           //at next update or at next call of layoutIfNeeded, 
                           //whichever comes first 
    [self layoutIfNeeded]; //Calls layoutSubviews if flag is set
}

Таким образом, если вы сделаете следующее, если вы измените свойство edgeInsets внутри блока анимации, будет изменена смена фрейма contentView.

[UIView animateWithDuration:2 animations:^{
    customView.edgeInsets = UIEdgeInsetsMake(45, 17, 18, 34);
}];

Если вы не добавляете вызов layoutIfNeeded в методе setEdgeInsets, анимация не будет работать, потому что layoutSubviews будет вызван в следующем цикле обновления, что равнозначно вызову его вне блока анимации.

Если вы вызываете только layoutIfNeeded в методе setEdgeInsets, ничего не произойдет, поскольку флаг setNeedsLayout не установлен.