声明周期
视图树本身是暂态的,所以它的⽣命周期概念没什么意义。不过,渲染树中的节点拥有⽣命周期:节点从第⼀次被渲染开始存在,直到它们不再需要被显示为⽌,就是节点的整个⽣命周期。
然⽽,渲染树中节点的寿命和它们在屏幕上的可⻅性并不是⼀回事⼉。如果我们在⼀个 ScrollView 中渲染⼀个很⼤的 VStack,那么不管 VStack 的⼦视图内容是不是正被显示在屏幕上,渲染树中都将包含这个 VStack 的所有⼦视图。
VStack 会选择更“急切”地渲染内容,这点和 LazyVStack 这种懒加载的容器是相反的。但即使实在懒加载的 stack 中,渲染树中的节点在离开屏幕后也还是会被保留,以维持它们的状态 (我们会在状态章节中详细深⼊这些内容)。总的来说,渲染树中的节点有其⽣命周期,但它并不受我们的控制。
出于实际考虑,SwiftUI 提供了三个钩⼦⽅法 (hook) 来处理⽣命周期相关的事件:
- onAppear 在每次视图出现在屏幕上时执⾏。即使渲染树中的对应节点从未消失,这个钩⼦⽅法也可以对⼀个视图进⾏多次调⽤。⽐如,在
LazyVStack 或 List 中的视图重复地滚动离开屏幕然后再回来时,每次onAppear 都会被调⽤。在 TabView 中切换 tab 时也是如此:不仅仅是第⼀次显示某个 tab,⽽是每次切换到某个 tab 时,⽽它的 onAppear 都会被
调⽤。 - onDisappear 在视图从屏幕上消失时执⾏。它和 onAppear 是对应关系,使⽤同样的规则 (就算背后的节点没有消失,这个⽅法也可能会被多次调⽤)。
- task 是前⾯两者与异步操作的结合。这个修饰器在 onAppear 会被调⽤的时间点创建⼀个新的异步任务 (task),并在 onDisappear 将被调⽤的时候取消这个任务。 (需要版本>iOS15)
生命周期中的 task
通过 task 修饰器开发者可以添加在视图“出现之前”的异步操作(和OnAppear(perform:)
相似)。
// 例子来自: https://fatbobman.com/zh/posts/mastering_swiftui_task_modifier/
struct ContentView: View {
@State var message: String?
let url = URL(string: "https://news.baidu.com/")!
var body: some View {
VStack {
if let message = message {
Text(message)
} else {
ProgressView()
}
}
.padding()
.task {
do {
var lines = 0
for try await _ in url.lines { // 读取指定 url 的内容
lines += 1
}
try? await Task.sleep(nanoseconds: 1_000_000_000) // 模拟更复杂的任务
message = "Received \(lines) lines"
} catch {
message = "Failed to load data"
}
}
}
}
// 例子来自: https://fatbobman.com/zh/posts/mastering_swiftui_task_modifier/
struct ContentView: View {
@State var message: String?
let url = URL(string: "https://news.baidu.com/")!
var body: some View {
VStack {
if let message = message {
Text(message)
} else {
ProgressView()
}
}
.padding()
.task {
do {
var lines = 0
for try await _ in url.lines { // 读取指定 url 的内容
lines += 1
}
try? await Task.sleep(nanoseconds: 1_000_000_000) // 模拟更复杂的任务
message = "Received \(lines) lines"
} catch {
message = "Failed to load data"
}
}
}
}
iOS17上@State可⽤于值和对象
SwiftUI 提供了好⼏种不同的状态包装类型,取决于状态是⼀个值还是⼀个对象,以及它是视图私有的状态还是是由外部传⼊的状态,应该选⽤不同的包装。不过,通常我们不需要直接处理这些包装器的类型,因为 SwiftUI 通过像是@State、@StateObject 和 @ObservedObject 这样的属性包装器将它们全部暴露出来了。
在 iOS 17 中,SwiftUI 与对象交互的⽅式已经完全改变。SwiftUI 不再依赖Combine 框架进⾏观察,⽽是采⽤基于宏的解决⽅案,这也使得@StateObject 和 @ObservedObject 属性包装器变得不再必要。@State 属性包装器现在可以⽤于值和对象,⽽在 iOS 17 之前,我们通常只将其⽤于值。由于 @State 和所有版本的 SwiftUI 都相关,我们将⾸先深⼊了解这个属性包装器,然后再对 iOS 17 之前和之后有关对象观察⽅⾯的情况进⾏区分。
@State
⽤于私有的视图状态值。
struct Counter: View {
// @State private var value = 0
// 不用属性包装器的写法
private var _value = State(initialValue: 0)
private var value: Int {
get { _value.wrappedValue }
nonmutating set { _value.wrappedValue = newValue }
}
var body: some View {
Button("increment \(value)") {
value += 1
}
}
}
struct Counter: View {
// @State private var value = 0
// 不用属性包装器的写法
private var _value = State(initialValue: 0)
private var value: Int {
get { _value.wrappedValue }
nonmutating set { _value.wrappedValue = newValue }
}
var body: some View {
Button("increment \(value)") {
value += 1
}
}
}
可以这么理解:
@State
属性包装器为我们完成了所有这些事情:它创建了⼀个 (⽤来存储实际的State 值的) 带下划线版本的属性,以及将 getter 和 setter 转发给 wrappedValue的计算属性。
为什么@State应当是私有属性:
按书中的说法:
视图的初始化⽅法中我们⽆法访问状态的当前值,这个传⼊值只会改变状态属性的初始值。⼀旦该视图的节点在渲染树中被创建,传⼊不同的初始值将没有任何效果,或者⾄少不是我们所期望的效果。
这⾥发⽣的只是初始值 (⽽不是实际值) 的改变,只有当节点被移除并重新插⼊到渲染树中时,这个初始值才会产⽣影响。
此外:另外一种写法是不可以的,即使编译器不报错:
我们之前就知道,self.value = value 语句实际上会被转变为self._value.wrappedValue = value
。从编译器的⻆度来看,这个语句没什么问题,但是它还是没法做到我们所期望的事情。不过,从它⽆法⼯作的原因中,我们可以得到⼀个宝贵的教训。
@Observable
Observable 宏是 SwiftUI 中⽤于观察对象状态的机制。它是 Observation 框架的⼀部分,于 WWDC23 推出,是我们此前⼀直使⽤的整个基于 Combine 框架的对象观察模型的替代品。
@State修饰的场景
@Status和状态管理关系:
@State
用于管理视图内部的局部状态。这种状态的生命周期与视图的生命周期紧密相连。当视图被创建时,@State
所包装的状态变量也会被初始化;当视图被销毁时,与之相关的@State
状态也会被释放。例如,在一个只在特定页面显示的计数器中,该计数器的状态使用@State
管理,当这个页面被关闭(视图销毁),计数器的状态也就不再存在。
@Observable
class ObservalbelViewModel {
var name: String = "1"
}
struct ObservalbelView: View {
@State private var viewModel: ObservalbelViewModel = .init()
var body: some View {
VStack {
Text(viewModel.name)
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
// 之前的写法:
class StatesObjectViewModel: ObservableObject {
@Published var name: String = "1"
}
struct StatesObjectView: View {
@StateObject var viewModel: StatesObjectViewModel = .init()
var body: some View {
VStack {
Text(viewModel.name)
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
@Observable
class ObservalbelViewModel {
var name: String = "1"
}
struct ObservalbelView: View {
@State private var viewModel: ObservalbelViewModel = .init()
var body: some View {
VStack {
Text(viewModel.name)
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
// 之前的写法:
class StatesObjectViewModel: ObservableObject {
@Published var name: String = "1"
}
struct StatesObjectView: View {
@StateObject var viewModel: StatesObjectViewModel = .init()
var body: some View {
VStack {
Text(viewModel.name)
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
不用@State
修饰的场景
import SwiftUI
@Observable
class ObservalbelViewModel {
var name: String = "1"
}
struct ObservalbelView: View {
var viewModel: ObservalbelViewModel
var body: some View {
VStack {
Text(viewModel.name)
ObservalbelView2(viewModel: viewModel)
}
}
}
struct ObservalbelView2: View {
var viewModel: ObservalbelViewModel
var body: some View {
VStack {
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
#Preview {
let model = ObservalbelViewModel()
ObservalbelView(viewModel: model)
}
import SwiftUI
@Observable
class ObservalbelViewModel {
var name: String = "1"
}
struct ObservalbelView: View {
var viewModel: ObservalbelViewModel
var body: some View {
VStack {
Text(viewModel.name)
ObservalbelView2(viewModel: viewModel)
}
}
}
struct ObservalbelView2: View {
var viewModel: ObservalbelViewModel
var body: some View {
VStack {
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
#Preview {
let model = ObservalbelViewModel()
ObservalbelView(viewModel: model)
}
在没有 @State 的情况下,SwiftUI 对于这个对象没有任何⽣命周期的责任,当视图的 body 被执⾏并且访问对象的属性时,观察会⾃动发⽣。因此,计数器视图的渲染节点⾃身不需要维护任何状态。
常见使用错误的场景
State 和 Observable 就观察对象⽽⾔,我们主要看到有两类常⻅的错误:
- 使⽤ @State 来处理从外部传⼊的对象(对象的⽣命周期已经由视图外部进⾏管理时,依然使⽤了@State)
- 不使⽤ @State 来处理视图内部私有状态的对象(在对象的⽣命周期没有在视图外部进⾏任何管理时,却仍然不使⽤ @State)
第一个错误的原因上面讲过:
初始化方法只能改变初始值,如果屏幕已经渲染了数值,此时再改变初始值不会有影响(由于外边声明的时候是调用的init方法,如果参数值改变,view也不会改变)。
下图示例:除了初始化生效了,后续改变改变入参值,子视图的count没有更新(因为后续改变的是init初始值)
第二个错误: 生命周期没有和外部关联,视图重建后会丢失状态。想要解决这个问题,我们可以从外部传⼊视图模型,或从某个全局的 model 对象中获取这个模型,或者也可以采取类似的其他⽅法。但⽆论如何,在视图中,如果模型属性没有使⽤ @State 声明,那么视图模型的⽣命周期必须要在视图之外的某个地⽅进⾏管理。
@Observable
class ObservalbelViewModel {
var name: String = "1"
}
struct ObservalbelView: View {
@State var showSubView: Bool = true
var body: some View {
VStack {
if showSubView {
ObservalbelView2()
}
Divider()
Button.init {
showSubView.toggle()
} label: {
Text("reset button")
}
}
}
}
struct ObservalbelView2: View {
var viewModel: ObservalbelViewModel
// 生命周期没有和外部关联
init() {
self.viewModel = ObservalbelViewModel()
}
var body: some View {
VStack {
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
@Observable
class ObservalbelViewModel {
var name: String = "1"
}
struct ObservalbelView: View {
@State var showSubView: Bool = true
var body: some View {
VStack {
if showSubView {
ObservalbelView2()
}
Divider()
Button.init {
showSubView.toggle()
} label: {
Text("reset button")
}
}
}
}
struct ObservalbelView2: View {
var viewModel: ObservalbelViewModel
// 生命周期没有和外部关联
init() {
self.viewModel = ObservalbelViewModel()
}
var body: some View {
VStack {
Button("click me \(viewModel.name)") {
viewModel.name += "C"
}
}
}
}
ObservableObject 协议
在 iOS 17 之前,我们需要使⽤ ObservableObject 协议,并组合使⽤@StateObject 或者 @ObservedObject 属性包装器,来观察对象的状态变化。
@StateObject
@StateObject 属性包装器的⼯作⽅式与 @State ⼤致相同。并且@StateObject 也只应该⽤于私有的视图状态
@ObservedObject
@ObservedObject 属性包装器要⽐ @StateObject 简单得多:它没有初始值的概念,也不需要在多次渲染之间维持被观察对象的存在。它所做的事情,就只有订阅这个对象的 objectWillChange publisher,并且在这个 publisher 发出事件时重新渲染视图。这些特性决定了,当我们 (把 iOS 17 之前的版本作为⽬标平台)想要明确地从外部将对象传递到视图内部时,@ObservedObject 是唯⼀正确的⼯具。
视图更新和性能
我们将所有的状态都放在⼀个庞⼤的可观察对象中 (并使⽤“传统”的@StateObject 或 ObservedObject 属性包装器),那么在任何更改发⽣时,就算这个特定更改可能只会影响观察该对象的视图之中的⼀⼩部分,SwiftUI 都必须重新渲染所有视图。当性能成为问题时,将状态分解为较⼩的单元可以帮助我们实现更细粒度的视图更新。@Observable 宏在很⼤程度上缓解了这个问题,因为它将观察从对象层级转移到了对象的各个属性上。
另外也需要着重注意,只应该向⼦视图传递它实际所需要的数据。例如,假设我们有⼀个包含许多属性的⼤型结构体,某个视图只需要从这个结构体中取⼀个值,但我们还是把整个结构体层层向下传递,那么每次当这个结构体中有任何东⻄发⽣变化,这个视图都会被重新渲染。这种情况其实可以通过在视图上明确定义我们所需要的那⼀个属性来避免;同时,这样做也会给我们带来额外的好处:在预览中显示这个视图也会变得更加容易。因此,将较⼤的视图拆分为依赖于不同⼦状态的多个⼦视图是有意义的。
性能分析:如何找到哪些视图的 body 正在被执⾏
第⼀种选择,是在视图的 body 中插⼊⼀条 print 语句:
如果你想寻找视图 body 被重新执⾏的原因,可以像这样,在视图的 body ⾥使⽤ Self._printChanges()
API: