- Published on
onAppear的调用时机
- Authors
- Name
说明
本文严重参考了肘子哥的文章 onAppear 的调用时机。加上了一些自己的理解,出于学习和强化理解的目的。
onAppear这个modifer在SwiftUI中使用得非常频繁,官方文档中有这样一段话:
The exact moment that SwiftUI calls this method depends on the specific view type that you apply it to, but the action closure completes before the first rendered frame appears.
它的闭包准备的调用时机取决于视图的类别,但是这个闭包会执行完成在第一个渲染帧出现之前。
视图的生命周期经历的阶段
在SwiftUI中,一个视图在它的生命周期中通常会经历4个阶段:
1.创建实例
对于要显示的视图,即处于视图树中的可显示分支中的视图,都会经历的一个阶段。
SwiftUI可能会多次创建视图实例,我记得在ChaoCode中有讲到,当一个视图有需要变更的时候,SwiftUI一般不会更新视图,而是直接丢弃一个视图来重新创建。这个观点还需要待求证。
由于惰性视图的优化机制,对于尚未处于可见区域的子视图,SwiftUI不会创建其实例
2.求值
由于SwiftUI的视图实际上是一个函数,SwiftUI需要对视图进行求值(调用body属性)并保留计算结果。
当视图的依赖(Soucre of truth)发生变化后,SwiftUI会重新计算视图结果值,并与旧值进行比较。
如发生变化,则用新值替换旧值。这和创建实例是不同的过程,一个是对其中的属性进行求值,一个是实例化。
上面提到的ChaoCode的观点,应该就是说的这种情况。
3.布局
完成第2步的视图值的计算后,SwiftUI将进入到布局的阶段。
大概过程如下:父视图向子视图提供建议尺寸,子视图返回需求尺寸,最终计算出完整的布局结果。
4.渲染
SwiftUI调用更加底层的API,将视图在呈现在屏幕上的过程。此过程严格意义上已经不属于SwiftUI的管理范畴了。
onAppear的调用时机
onAppear是相对于我们的视图来说的,这会让我们误认为,其会在视图渲染后(肉眼可见)才被调用,但在SwiftUI中,onAppear实际上是在渲染前被调用的。 这时的appear更像是针对SwiftUI系统来说的。这个名字如果取成 onAppearBeforeReander我觉得更加合理,但是这样的命名长度太长了。
这也印证了官方文档的描述:
The exact moment that SwiftUI calls this method depends on the specific view type that you apply it to, but the action closure completes before the first rendered frame appears.
如何来求证这个结果呢
判断SwiftUI视图正在求值
可以通过下面的代码,来判断视图正在进行求值
VStack {
let _ = print("evaluate")
}
判断SwiftUI视图正处于布局阶段
在SwiftUI4.0中,SwiftUI提供了Layout协议,允许我们创建自定义布局容器,能过创建符合这个协议的实例,我们可以判断到当前视图处于布局阶段。
struct MyLayout: Layout {
let name: String
//sizeThatFits 与 Layout 协议的 sizeThatFits 调用时机一致,都是在布局过程中,父视图向子视图询问需求尺寸时访问
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
print("3or4\(name) layout")
return .init(width: 100, height: 100)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
}
}
上面的代码创建了一个固定返回 100 * 100 需求尺寸的布局容器,在父视图询问其需求尺寸时将通过控制台报告给我们。
判断SwiftUI视图正准备渲染
尽管 SwiftUI 视图并没有提供可以展示该过程的 API,不过我们可以利用 UIViewControllerRepresentable 协议来包装一个 UIViewController ,并通过它的生命周期回调方法来确定当前的状态。
struct ViewHelper: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> HelperController {
return HelperController()
}
func updateUIViewController(_ uiViewController: HelperController, context: Context) {
}
//sizeThatFits 与 Layout 协议的 sizeThatFits 调用时机一致,都是在布局过程中,父视图向子视图询问需求尺寸时访问
func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: HelperController, context: Context) -> CGSize? {
print("5helper layout")
return .init(width: 50, height: 50)
}
typealias UIViewControllerType = HelperController
}
viewWillAppear 则是在 UIViewController 被呈现前( 可以理解为渲染前 ),会由 UIKit 调用。
通过 UIViewControllerRepresentable 封装的“视图”并非真正的视图,对于 SwiftUI 来说,它就是一块给出了需求尺寸的黑洞,因此并不存在求值一说。
创建一个Demo,来验证这个过程
struct LayoutTest: View {
var body: some View {
MyLayout(name: "outer") {
let _ = print("1outer evaluate")
MyLayout(name: "inner") {
let _ = print("2inner evaluate")
ViewHelper()
.onAppear {
print("7helper onAppear")
}
}
.onAppear {
print("8inner onAppear")
}
}
.onAppear {
print("6outer onAppear")
}
}
}
控制台输出如下:
1outer evaluate
2inner evaluate
3or4outer layout
3or4inner layout
5helper layout
6outer onAppear
7helper onAppear
8inner onAppear
9will appear(render)
结论
通过上面的输出,可以清楚地了解视图处理的全过程:
- SwiftUI 首先对视图进行求值( 由外向内 )
- 在全部求值结束后开始进行布局( 由父视图到子视图 )
- 在布局结束后,调用视图对应的 onAppear 闭包( 顺序不明,不要假定 onAppear 之间的执行顺序 )
- 渲染视图 由此可以证明,onAppear 确实是在布局之后,渲染之前被调用的。