WWDC Quick Look 💓 By SwiftGGTeam
Demystify SwiftUI

Demystify SwiftUI

观看原视频

Highlight

SwiftUI 的渲染行为由三个核心概念决定:Identity(身份标识)决定视图是否复用,Lifetime(生命周期)决定 State 何时重置,Dependency(依赖关系)决定何时刷新。理解这三者可以消除大部分 SwiftUI 中的诡异 bug。

核心内容

写 SwiftUI 时,开发者经常遇到三类问题:状态莫名重置、动画不触发、视图刷新过于频繁。这些问题表面看是 bug,根源往往是对 SwiftUI 核心机制理解不够。

这场 session 不讲新 API,而是深入剖析 SwiftUI 的底层哲学。

Identity:SwiftUI 如何识别视图

Identity 回答一个问题:两次渲染中,这个视图和那个视图是同一个吗?

SwiftUI 使用两种 identity:

结构性 identity:由视图在视图树中的位置决定。同一个位置,就是同一个视图。

显式 identity:通过 .id() 修饰符指定。当 id 变化时,SwiftUI 认为这是一个全新视图。

ForEach(items) { item in
    RowView(item: item)
}

这里 item 的 id 就是每个 RowView 的 identity。如果你把 items 重新排序,SwiftUI 会复用已有的视图实例,只更新数据。

但如果写成:

ForEach(0..<items.count, id: \.self) { index in
    RowView(item: items[index])
}

当数组中间插入一个元素时,后面所有元素的 index 都变了,identity 变了,SwiftUI 会销毁重建这些视图,State 全部丢失。

Lifetime:State 的生命周期

@State@StateObject 的生命周期与视图的 identity 绑定。identity 不变,State 就保留;identity 变化,State 重置。

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        Button("Count: \(count)") {
            count += 1
        }
        .id(someValue) // 当 someValue 变化,count 重置为 0
    }
}

Dependency:SwiftUI 如何知道该刷新

SwiftUI 在 body 执行期间自动追踪所有被读取的依赖。当依赖变化时,视图自动刷新。

struct UserView: View {
    @ObservedObject var user: User

    var body: some View {
        Text(user.name) // SwiftUI 记住了:这个视图依赖 user.name
    }
}

如果 user.name 变化,SwiftUI 刷新这个视图。但如果 user.age 变化,而这个视图中没有读取 user.age,SwiftUI 不会刷新它。

详细内容

常见的 Identity 陷阱

条件分支切换视图类型

if isEditing {
    TextField("Name", text: $name)
} else {
    Text(name)
}

isEditing 切换时,TextField 和 Text 在视图树中占据同一位置,但类型不同。SwiftUI 会销毁 TextField 创建 Text,再切换回来时销毁 Text 创建新的 TextField。TextField 的聚焦状态会丢失。

修复方式:给两个视图加上稳定的 id。

if isEditing {
    TextField("Name", text: $name)
        .id("nameField")
} else {
    Text(name)
        .id("nameField")
}

这样 SwiftUI 知道它们是同一个逻辑视图,只是类型不同。

ForEach 使用不稳定的 id

ForEach(items.indices, id: \.self) { index in
    // index 作为 id,插入元素后全部重建
}

修复方式:使用数据本身的稳定标识。

ForEach(items, id: \.id) { item in
    // item.id 是稳定的
}

@StateObject vs @ObservedObject

struct ParentView: View {
    @StateObject private var model = ViewModel()

    var body: some View {
        ChildView(model: model)
    }
}

struct ChildView: View {
    @ObservedObject var model: ViewModel

    var body: some View {
        Text(model.title)
    }
}

关键点:

  • @StateObject 在视图首次创建时初始化,之后复用同一实例
  • @ObservedObject 只是观察外部传入的对象,不管理其生命周期
  • 绝不要在 body 中创建 ObservableObject 实例,每次刷新都会创建新的

Dependency 的精确追踪

SwiftUI 的依赖追踪是属性级别的。

struct DetailView: View {
    let item: Item

    var body: some View {
        VStack {
            Text(item.title)
            Text(item.description)
        }
    }
}

如果 item 是值类型(struct),每次父视图传入一个新的 item(即使内容相同),SwiftUI 都会刷新 DetailView

如果 item 是引用类型(class),SwiftUI 比较的是引用地址。只有当传入不同实例时才刷新。

核心启发

1. 用稳定的 identity 设计列表

在实现可编辑列表时,确保每个 cell 的 identity 基于数据的业务 ID,而非数组索引。这样插入、删除、排序时,SwiftUI 能正确复用视图,动画也更流畅。入口 API:ForEach(items, id: \.id)

2. 把 ViewModel 的生命周期与视图解耦

@StateObject 在根视图创建 ViewModel,通过 @ObservedObject 或环境对象传递给子视图。这样即使子视图因 identity 变化被重建,数据也不会丢失。入口 API:@StateObject + environmentObject()

3. 用 EquatableView 优化大量静态内容

对于包含大量子视图但很少变化的容器,实现 Equatable 协议并包装为 EquatableView,让 SwiftUI 跳过不必要的 diff。入口 API:static func == + .equatable()

4. 诊断 State 重置问题

当发现 State 意外重置时,检查三处:是否使用了 .id() 并变化了值、ForEach 的 id 是否稳定、条件分支中的视图是否共享同一位置。用 Xcode 的 SwiftUI 调试工具查看视图树的 identity 变化。

5. 设计可预测的刷新边界

把频繁变化的数据(如计时器、传感器数据)隔离到独立的子视图中,避免触发整个页面的刷新。入口 API:将动态部分提取为独立 View,只传递必要的数据。

关联 Session

评论

GitHub Issues · utterances