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

105

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

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

Tarfa
источник

Ответы:

104

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

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

layoutSubviewsэто метод, с помощью которого вы делаете все самое интересное. Это эквивалент drawRectмакета, если хотите. Тривиальный пример:

-(void)layoutSubviews {
    // Child's 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представление, чтобы заставить его (и, при необходимости, его супервизоры) быть выложены немедленно.

n8gray
источник
2
Спасибо - рад, что кто-то наконец ответил на это. Тем временем мне также пришлось углубиться в setNeedsDisplay, который вызывает вызов drawRect, и contentMode. Только contentMode = UIViewContentModeRedraw вызовет вызов setNeedsDisplay при изменении границ представления. Другие варианты contentMode только вызывают масштабирование, смещение на некоторую величину или выравнивание по краю. Мой пользовательский UITableViewCell не хотел перерисовывать при изменении ориентации, он только масштабировался, пока я не установил для contentMode значение UIViewContentModeRedraw.
Tarfa
Я думаю, что, возможно, [self.subview1 layoutSubviews] в вашем коде следует заменить на [self.subview1 setNeedsLayout]. Поскольку layoutSubviews предназначен для переопределения, но не предназначен для вызова из другого представления или из другого представления. Либо фреймворк определяет, когда его вызывать (группируя несколько запросов макета в один запрос для повышения эффективности), либо вы делаете это косвенно, вызывая layoutIfNeeded где-нибудь в иерархии представлений.
Tarfa
Исправление к моему вышеупомянутому комментарию: «... Поскольку layoutSubviews предназначен для переопределения или вызова из себя, но не предназначен для
вызова
Я действительно не уверен [subview1 layoutSubviews]. Вполне может быть, drawRectчто вы не должны называть это напрямую. Но я не уверен, setNeedsLayoutприведет ли вызов во время фазы макета к тому, что представление будет размещено на той же фазе макета, или это будет отложено до следующего. Было бы неплохо увидеть ответ от того, кто действительно понимает, как все это работает ...
n8gray
34

Я хотел бы добавить к ответу 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 не установлен.

Quentinadam
источник
4
Или вы должны просто иметь возможность вызывать [self layoutIfNeeded]внутри блока анимации после установки краевых вставок.
Скотт Фистер