解释
更新布局总会重新触发layoutSubviews
方法。
-
layoutSubviews
继承于UIView
的子类重写,进行布局更新,刷新视图。如果某个视图自身的bounds
或者子视图的bounds
发生改变,那么这个方法会在当前runloop
结束的时候被调用。为什么不是立即调用呢?因为渲染毕竟比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个周期,所有UI控件在布局上的变动统一生效并且在视图上更新,苹果通过这种高性能的机制保障了视图渲染的流畅性。
从上图中可以看到,runloop
的observer
回调=>CoreAnimation
渲染引擎一次事务的提交=>CoreAnimation
递归查询图层是否有布局上的更新=>CALayer
layoutSublayers
=>UIView
layoutSubviews
这样一个调用的流程。从这里也可以看到UIView
其实就是相当于CALayer
的代理。
顺便看一眼drawRect
方法的调用栈,从CA::Layer::layout_and_display_if_needed
方法之前都是一样的。
-
setNeedsLayout
标记为需要重新布局,异步调用layoutIfNeeded
刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop
之内的所有布局和UI上的更新只会刷新一次,layoutSubviews
一定会被调用。 -
layoutIfNeeded
如果有需要刷新的标记,立即调用layoutSubviews
进行布局(如果没有标记,不会调用layoutSubviews
)。
关键点
-
layoutIfNeeded
不一定会调用layoutSubviews方法。 -
setNeedsLayout
一定会调用layoutSubviews
方法(有延迟,在下一轮runloop结束前)。 - 如果想在当前
runloop
中立即刷新,调用顺序应该是
[self setNeedsLayout];
[self layoutIfNeeded];
反之可能会出现布局错误的问题。
前言:
开发得跟view打交道,我们也经常看到苹果官方代码有layout方法的相关调用,但是大家可知道什么时候调用,什么时候需要吗?针对网上大部分资料讲得不够清晰,我决定用Demo来讲解
一、layoutSubviews
不能直接调用这个方法。如果强制刷新布局,请调用 setNeedsLayout,如果想马上刷新界面,请调用layoutIfNeeded
二、setNeedsLayout跟layoutIfNeded
setNeedsLayout
调整视图的子视图的布局时,是在应用程序的主线程调用此方法的。此方法记录调整的布局请求并立即返回。注意,此方法不会强制立即更新,而是等待下一个更新周期才进行刷新页面布局。此行为允许你将所有的布局更新合并到一个更新周期,这很适合用来优化性能。
首先我们在Main.storyboard
拖取出一个adjust Height
按钮以及带有约束的一个viewRed
如图:
把viewRed
的Constraints
拖到ViewController.swift
,并作为属性
下面我用一段代码验证下
@IBAction func adjustedBtnClick(_ sender: Any) {
view.layoutIfNeeded()
if self.redViewHeight.constant == 30.0 {
self.redViewHeight.constant = self.view.frame.height - 90
}else {
self.redViewHeight.constant = 30.0
}
UIView.animate(withDuration: 2.0) {
self.view.layoutIfNeeded()
}
}
当按下按钮时,你看到的第一件事是调用view.layoutIfNeeded()。请记住,此方法强制立即布局并显示更新。你可能会想知道为什么在我们进行约束更改之前调用它呢,因为苹果认为这是一个最佳做法,以确保任何以前的更新等待更新周期的完成,所以我已经添加了它。
当这样的约束被更新时,它会自动执行相当于setNeedsLayout的操作,因此在下一个更新周期内不需要更新视图,所以不用额外添加代码,我们就会看到视图约束更新,但它不会有动画效果。但在我们的例子中,添加了2秒的动画块UIView.animate
,在该块内,我们通过layoutIfNeeded方法强制立即布局。由于此布局同步发生,因此在动画块中捕获来自约束更改的帧移动,因此如果你现在运行应用程序,则可以看到红色视图在2秒钟内如何变大变小。
效果如下:
当我们把动画块代码替换成
UIView.animate(withDuration: 2.0) {
self.view.setNeedsLayout()
}
现在我们在动画块中正在做的是将视图标记为需要布局更新,但不会立即强制执行。相反,setNeedsLayout
方法返回,视图只是在列表中,以在下一个更新周期中进行更新。净效果是在动画块中不会发生动画,因为该块中的视图没有更改。
效果如下:
在这种情况下单击按钮将立即根据更新的约束更新视图大小,而不是动画更新。等一下,如果我们没有使用layoutIfNeeded,为什么会立即?
对我们而言,红色视图的大小变化立即发生。起初它似乎是反直觉的,因为我们没有使用layoutIfNeeded
强制立即更新。但是,我们没有在动画块的上下文中进行视图更新,也就是说刚好是一个周期,所以它似乎是立即的,跟没有UIView.animate
这段代码效果是一样的。因为我们标记为更新,更新周期的发生的地方也是动画开始的地方。
因此,由于我们的代码已经标记该视图需要通过setNeedsLayout
进行布局更新,所以是在更新周期中立即启动视图更新,而不是从更新周期开始约束更改和帧移动的动画。
刷新子对象布局:
-layoutSubviews方法:这个方法,默认没有做任何事情,需要子类进行重写
------------ 2018.8.25 更新 ------------
这个方法在iOS 5.1和更早版本上没有任何作用。子类可以根据需要重写此方法,以执行更精确的子视图布局。只有当子视图的自动调整、约束的行为不能满足你时,你才应该重写此方法
-setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
-layoutIfNeeded方法:如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)关于在什么的情景会使用该方法,stackoverflow回答的比较详细
- 如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局
- 在视图第一次显示之前,标记肯定是“需要刷新”的,所以直接调用[view layoutIfNeeded]就会进行立即更新