Discover Observation in SwiftUI
Swift & UI 进阶 20m

探索 SwiftUI 中的 Observation

Discover Observation in SwiftUI

2023年6月5日

在 Apple 官方观看视频

一句话判断

@Observable 宏彻底改变了 SwiftUI 的数据流——不需要 @Published、不需要 ObservableObject、不需要 @ObservedObject,Swift 编译器自动追踪属性访问,SwiftUI 只在用到的属性变化时才更新视图。这是今年对 SwiftUI 开发体验影响最大的改动。

这场 Session 讲了什么

Session 介绍了 Swift 5.9 中的 Observation 框架及其与 SwiftUI 的深度集成。

@Observable 宏

  • 在类声明上加 @Observable,编译器自动生成属性观察代码。
  • SwiftUI 在执行视图 body 时追踪所有访问的 Observable 属性。
  • 只有被追踪的属性变化时才触发视图更新——精确到实例级别。

属性包装器简化为三种

  1. @State:视图拥有的状态。当模型需要由视图生命周期管理时使用。
  2. @Environment:全局环境值。当模型需要在多个视图间共享时使用。
  3. @Bindable:创建绑定的轻量包装。需要 Binding 时使用(如 TextField)。
  4. 不需要包装器:大多数情况下直接使用 Observable 模型实例即可。

高级用法

  • Observable 模型可以放在数组中——SwiftUI 追踪数组中每个实例的属性访问。
  • Observable 模型可以包含其他 Observable 模型。
  • 计算属性如果由存储属性组成,自动支持观察。如果依赖外部数据源,需要手动调用 Observation 框架的 access/mutate 方法。

从 ObservableObject 迁移

  • ObservableObject + @Published@Observable
  • @ObservedObject → 直接使用属性
  • @EnvironmentObject@Environment
  • @StateObject@State

值得深挖的点

**“只有被访问的属性变化才更新视图”**是性能提升的核心。之前 ObservableObject@Published 属性变化会通知所有订阅者,即使某个视图没有用到变化的属性。Observation 让 SwiftUI 知道每个视图具体访问了哪些属性,只在这些属性变化时才重新计算 body。这对大型视图树来说性能提升非常显著。

不需要属性包装器的场景是最让人意外的设计。在大多数情况下,你只需要一个普通的属性引用 Observable 实例,不需要任何包装器。SwiftUI 的属性访问追踪机制会自动工作。只有在需要 State 生命周期管理、Environment 全局共享、或 Binding 绑定时才需要包装器。

数组中的 Observable 实例也是按实例追踪的。如果你有一个 [Donut] 数组,修改第三个 donut 的名字只会更新访问了那个实例的那个属性的视图。这种精确度在列表和集合视图中特别有价值。

代码片段

使用 @Observable 宏:

import Observation

@Observable  // 一个宏搞定——不需要 @Published
class FoodTruckModel {
    var donuts: [Donut] = []
    var orders: [Order] = []
    var userName: String = ""

    // 计算属性自动支持观察(如果由存储属性组成)
    var orderCount: Int {
        orders.count  // orders 变化时,访问 orderCount 的视图会更新
    }
}

在 SwiftUI 中使用(不需要属性包装器):

struct DonutMenuView: View {
    // 直接使用——不需要 @ObservedObject 或 @EnvironmentObject
    var model: FoodTruckModel

    var body: some View {
        List(model.donuts) { donut in
            Text(donut.name)
        }
        // SwiftUI 自动追踪:这个视图访问了 model.donuts
        // 只有 donuts 变化时才会重新计算 body
        // orders 变化不会触发更新
    }
}

三种属性包装器的使用场景:

struct DonutEditorView: View {
    // 场景1:视图拥有的状态
    @State private var donutToAdd = Donut()

    // 场景2:全局环境
    @Environment var account: AccountModel

    // 场景3:需要 Binding
    @Bindable var donut: Donut

    var body: some View {
        Form {
            // 使用 @Bindable 创建绑定
            TextField("名称", text: $donut.name)

            // 使用 @State 管理生命周期
            TextField("新名称", text: $donutToAdd.name)
        }
    }
}

最佳实践

  • 新项目直接用 @Observable,不要再用 ObservableObject
  • 先不用任何属性包装器,只在需要 State/Environment/Binding 时才添加。
  • @State 用于视图拥有的状态,@Environment 用于全局共享,@Bindable 只在需要 Binding 时用。
  • 利用属性级追踪减少不必要的视图更新——大列表的性能会明显改善。
  • 迁移现有代码时,@StateObject 替换为 @State@ObservedObject 替换为普通属性,@EnvironmentObject 替换为 @Environment
  • 计算属性如果依赖外部数据源(非存储属性),需要手动调用 Observation 的 withObservationTracking

还有什么值得关注

  • “Write Swift macros”和”Expand on Swift macros” Session 介绍了宏的底层机制。
  • Observation 框架是 Swift 标准库的一部分,不依赖 SwiftUI——可以在非 UI 场景中使用。
  • @Observable 只能用于 class(不能用于 struct),因为它依赖引用语义来追踪实例。
  • 从 ObservableObject 迁移到 @Observable 通常是机械性的替换,风险较低。
  • 性能测试表明,Observation 在大型视图树中的更新效率比 ObservableObject 有显著提升。
WWDC 2023