UIView 的 setNeedsLayout、 layoutIfNeeded 和 layoutSubviews 之间的关系是什么?

谁能给出一个明确的解释之间的关系 UIView's setNeedsLayoutlayoutIfNeededlayoutSubviews方法?以及一个示例实现,其中将使用所有三个。谢谢。

让我感到困惑的是,如果我向自定义视图发送一条 setNeedsLayout消息,那么它在这个方法之后调用的下一个东西就是 layoutSubviews,跳过了 layoutIfNeeded。从文档中,我希望流程是 setNeedsLayout > 使得 layoutIfNeeded被调用 > 使得 layoutSubviews被调用。

47249 次浏览

I'm still trying to figure this out myself, so take this with some skepticism and forgive me if it contains errors.

setNeedsLayout is an easy one: it just sets a flag somewhere in the UIView that marks it as needing layout. That will force layoutSubviews to be called on the view before the next redraw happens. Note that in many cases you don't need to call this explicitly, because of the autoresizesSubviews property. If that's set (which it is by default) then any change to a view's frame will cause the view to lay out its subviews.

layoutSubviews is the method in which you do all the interesting stuff. It's the equivalent of drawRect for layout, if you will. A trivial example might be:

-(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 isn't generally meant to be overridden in your subclass. It's a method that you're meant to call when you want a view to be laid out right now. Apple's implementation might look something like this:

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

You would call layoutIfNeeded on a view to force it (and its superviews as necessary) to be laid out immediately.

I would like to add on n8gray's answer that in some cases you will need to call setNeedsLayout followed by layoutIfNeeded.

Let's say for example that you wrote a custom view extending UIView, in which the positioning of subviews is complex and cannot be done with autoresizingMask or iOS6 AutoLayout. The custom positioning can be done by overriding layoutSubviews.

As an example, let's say that you have a custom view that has a contentView property and an edgeInsets property that allows to set the margins around the contentView. layoutSubviews would look like this:

- (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);
}

If you want to be able to animate the frame change whenever you change the edgeInsets property, you need to override the edgeInsets setter as follows and call setNeedsLayout followed by 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
}

That way, if you do the following, if you change the edgeInsets property inside an animation block, the frame change of the contentView will be animated.

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

If you do not add the call to layoutIfNeeded in the setEdgeInsets method, the animation won't work because the layoutSubviews will get called at the next update cycle, which equates to calling it outside of the animation block.

If you only call layoutIfNeeded in the setEdgeInsets method, nothing will happen as the setNeedsLayout flag is not set.