WWDC Quick Look 💓 By SwiftGGTeam
Discover Observation in SwiftUI

Discover Observation in SwiftUI

观看原视频

Highlight

Swift 5.9 引入 @Observable 宏,让数据模型不再需要 @PublishedObservableObject,SwiftUI 自动按属性粒度追踪依赖,只刷新真正受影响的视图。

核心内容

以前的数据流痛点

ObservableObject 时,每个属性都要加 @Published。视图里要用 @ObservedObject@StateObject 包装。问题是:只要模型里任何一个 @Published 属性变化,整个视图都会刷新,即使这个视图根本没用到那个属性。

中间视图传模型时也一样。父视图传给子视图,子视图再传给孙视图,每次属性变化,整条链路上的视图都要重新计算 body。

@Observable 宏的解法

01:21@Observable 是 Swift 5.9 的宏,编译时自动展开为可观察类型。开发者只需要在类声明前加一行注解:

@Observable class FoodTruckModel {
    var orders: [Order] = []
    var donuts = Donut.all
}

不需要 @Published,不需要 ObservableObject 协议。所有存储属性默认可观察。

SwiftUI 执行视图 body 时,自动记录访问了哪些属性。之后只有这些被记录过的属性变化时,才会触发该视图的刷新。

02:56)如果 orders 数组增加了元素,但某个视图只读取了 donuts,那这个视图不会刷新。精确到属性级别的追踪,避免了大量不必要的更新。

计算属性也支持

03:12)计算属性如果由存储属性组成,Observation 自动追踪:

@Observable class FoodTruckModel {
    var orders: [Order] = []
    var donuts = Donut.all
    var orderCount: Int { orders.count }
}

orderCount 依赖 orders。视图读取 orderCount 时,SwiftUI 会追踪到底层的 orders 属性。orders 变化时,读取过 orderCount 的视图会刷新。

属性包装器简化为三种

04:41)Observable 类型配合 SwiftUI 时,只需要记住三个问题:

  1. 这个模型是视图自己的状态吗?是,用 @State
  2. 这个模型需要在全局环境共享吗?是,用 @Environment
  3. 这个模型只需要绑定吗?是,用 @Bindable

三个问题的答案都是否,就直接把模型作为视图的普通属性。

@State 的用法:

struct DonutListView: View {
    var donutList: DonutList
    @State private var donutToAdd: Donut?

    var body: some View {
        List(donutList.donuts) { DonutView(donut: $0) }
        Button("Add Donut") { donutToAdd = Donut() }
            .sheet(item: $donutToAdd) {
                TextField("Name", text: $donutToAdd.name)
                Button("Save") {
                    donutList.donuts.append(donutToAdd)
                    donutToAdd = nil
                }
                Button("Cancel") { donutToAdd = nil }
            }
    }
}

关键点:

  • @State 管理的属性生命周期和视图绑定
  • donutToAdd 是视图自己的状态,不是从外部传入的模型
  • sheet 里用 $donutToAdd.name 创建绑定

@Environment 的用法:

@Observable class Account {
    var userName: String?
}

struct FoodTruckMenuView: View {
    @Environment(Account.self) var account

    var body: some View {
        if let name = account.userName {
            HStack { Text(name); Button("Log out") { account.logOut() } }
        } else {
            Button("Login") { account.showLogin() }
        }
    }
}

关键点:

  • @Environment(Account.self) 从环境读取指定类型的值
  • 用类型本身作为环境键,也支持自定义键
  • account.userName 被访问,所以 userName 变化时视图刷新

@Bindable 的用法:

@Observable class Donut {
    var name: String
}

struct DonutView: View {
    @Bindable var donut: Donut

    var body: some View {
        TextField("Name", text: $donut.name)
    }
}

关键点:

  • @Bindable 只负责创建绑定,是最轻量的包装器
  • $donut.name 语法生成 Binding<String>
  • TextField 读取绑定显示值,用户输入时写回绑定

数组中的 Observable 模型

07:53)Observable 模型放在数组里也能精确追踪:

@Observable class Donut {
    var name: String
}

struct DonutList: View {
    var donuts: [Donut]

    var body: some View {
        List(donuts) { donut in
            HStack {
                Text(donut.name)
                Spacer()
                Button("Randomize") {
                    donut.name = randomName()
                }
            }
        }
    }
}

关键点:

  • SwiftUI 追踪数组中每个实例的属性访问
  • 点击 “Randomize” 只刷新对应行的视图
  • 其他行的 donut.name 没有变化,不会触发更新
  • 模型可以嵌套:Observable 模型包含其他 Observable 模型

手动 Observation

09:18)如果计算属性依赖外部数据源,需要手动标记访问和变化:

@Observable class Donut {
    var name: String {
        get {
            access(keyPath: \.name)
            return someNonObservableLocation.name
        }
        set {
            withMutation(keyPath: \.name) {
                someNonObservableLocation.name = newValue
            }
        }
    }
}

关键点:

  • access(keyPath:) 标记属性被读取
  • withMutation(keyPath:) 标记属性被修改
  • 绝大多数场景不需要手动写,编译器自动合成
  • 只有计算属性依赖非 Observable 外部数据时才需要

从 ObservableObject 迁移

10:25)迁移步骤很直接:

修改前:

class FoodTruckModel: ObservableObject {
    @Published var orders: [Order] = []
    @Published var donuts = Donut.all
}

修改后:

@Observable class FoodTruckModel {
    var orders: [Order] = []
    var donuts = Donut.all
}

视图层面的替换:

  • @ObservedObject → 直接作为普通属性,或改为 @Bindable(如果需要绑定)
  • @EnvironmentObject@Environment
  • @StateObject@State

大部分改动就是删除注解。代码更短,性能更好。

详细内容

完整的数据流决策流程

06:55)判断用哪个属性包装器的决策树:

// 场景 1:视图自己的状态
struct AddDonutSheet: View {
    @State private var newDonut = Donut()

    var body: some View {
        TextField("Name", text: $newDonut.name)
    }
}

// 场景 2:全局共享的数据
@main
struct FoodTruckApp: App {
    let account = Account()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(account)
        }
    }
}

struct ProfileView: View {
    @Environment(Account.self) var account

    var body: some View {
        Text(account.userName ?? "Guest")
    }
}

// 场景 3:只需要绑定
struct DonutEditor: View {
    @Bindable var donut: Donut

    var body: some View {
        Form {
            TextField("Name", text: $donut.name)
            TextField("Flavor", text: $donut.flavor)
        }
    }
}

// 场景 4:不需要包装器
struct DonutRow: View {
    let donut: Donut

    var body: some View {
        Text(donut.name)
    }
}

关键点:

  • newDonut 由视图创建和管理,用 @State
  • account 在多个视图间共享,用 .environment() 注入,@Environment 读取
  • donut 从父视图传入,需要双向绑定,用 @Bindable
  • donut 只读显示,不需要任何包装器

性能优势

04:05)Observation 的精确追踪带来显著性能提升:

@Observable class FoodTruckModel {
    var orders: [Order] = []      // View A 读取
    var donuts = Donut.all        // View B 读取
    var revenue: Decimal = 0      // View C 读取
}
  • orders 变化 → 只有 View A 刷新
  • donuts 变化 → 只有 View B 刷新
  • revenue 变化 → 只有 View C 刷新

ObservableObject 时,三个属性都标 @Published,任何一个变化,三个视图都会刷新。Observable 把刷新范围缩小到最小。

核心启发

  1. 重构现有 SwiftUI 项目的数据层

    • ObservableObject + @Published 模型迁移到 @Observable
    • 删除视图中大量 @ObservedObject@EnvironmentObject
    • 入口:逐个模型替换,从叶子视图开始向上推进
  2. 构建高性能的大列表应用

    • 列表中每个 cell 对应一个 Observable 模型实例
    • 修改单个 cell 的数据时,只有该 cell 刷新
    • 入口:List(items) { ItemRow(item: $0) }ItemRow 直接接收 Item 实例
  3. 设计可复用的编辑组件

    • @Bindable 包装模型,组件内部用 TextFieldToggle 等绑定控件
    • 组件不依赖特定的状态管理方式,父视图决定用 @State 还是 @Environment
    • 入口:struct EditorView: View { @Bindable var item: Item }
  4. 实现跨视图共享的用户状态

    • @Environment 在根视图注入用户账户、设置等全局状态
    • 子视图按需读取特定属性,只在该属性变化时刷新
    • 入口:.environment(userAccount) + @Environment(Account.self)

关联 Session

评论

GitHub Issues · utterances