Observation 的价值不只是少写几个 @Published。它改变了 SwiftUI 读取状态的方式:视图在执行 body 时访问了哪些属性,系统就跟踪哪些属性。状态更新后,依赖这些属性的视图才需要重新计算。要真正得到这份好处,关键仍然是明确“谁拥有状态、谁只读取状态、谁可以修改状态”。

从所有权开始

下面的模型由页面创建并持有,因此使用 @State 保存。@Observable 负责属性访问跟踪;@MainActor 则明确所有 UI 状态只能在主执行器上修改。

import Observation
import SwiftUI

@Observable
@MainActor
final class ReadingList {
    var books: [String] = []
    var draft = ""

    var canAdd: Bool {
        !draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    func addBook() {
        guard canAdd else { return }
        books.append(draft)
        draft = ""
    }
}

struct ReadingListView: View {
    @State private var model = ReadingList()

    var body: some View {
        @Bindable var model = model

        Form {
            TextField("Book title", text: $model.draft)
            Button("Add", action: model.addBook)
                .disabled(!model.canAdd)

            ForEach(model.books, id: \.self) { book in
                Text(book)
            }
        }
    }
}

这里的 @Bindable 不拥有模型。它只为当前视图需要编辑的属性生成绑定。模型依旧由 @State 保存,视图重建时不会无意创建另一份阅读列表。

子视图只接收所需能力

不要因为模型已经可以观察,就把整个模型传遍视图树。只显示数量的子视图可以接收一个整数;负责录入的子视图才需要可绑定模型。较窄的参数让依赖关系更直观,也让 Preview 和单元测试更容易准备数据。

struct BookCount: View {
    let count: Int

    var body: some View {
        Text("\(count) books")
            .foregroundStyle(.secondary)
    }
}

这也避免了一个常见误区:Observation 能减少不必要的更新,却不能替代模块设计。如果一个对象同时负责网络、缓存、导航和十几个页面的状态,换成 @Observable 后仍然是一个过深、过重的依赖。

审查实际依赖范围

迁移后可以在关键视图中暂时使用 SwiftUI 的变化诊断工具,观察哪些输入导致 body 重算。频繁变化的计时器、下载进度或搜索输入不应让完全无关的页面区域一起刷新。若仍然出现大范围更新,先检查父视图是否读取了整个模型并把派生值广泛传递,再决定是否拆分模型。拆分依据应是生命周期和业务职责,而不是单纯追求更小文件。

环境注入也要保持克制。把模型放进 environment 很适合应用级会话或明确的功能子树,但会让依赖在初始化器中不可见。对于可复用组件,显式参数通常更易理解;对于页面协调器,环境可能更自然。关键是团队能够从一个入口判断模型由谁创建、何时释放,以及退出账号时由谁清空。

异步工作放在边界之外

UI 模型可以协调加载状态,但网络客户端应是独立依赖。先获取值,再回到主执行器更新模型,比在模型里隐藏一个无法替换的全局客户端更容易测试。测试时可以直接构造 ReadingList,调用同步领域方法,并断言 booksdraft;网络行为则在客户端自己的测试中验证。

迁移旧代码时不必一次替换所有 ObservableObject。可以从一个叶子页面开始:移除 @Published,用 @Observable 标记模型,把拥有者改为 @State,只在需要双向绑定的位置引入 @Bindable。随后利用 Instruments 和实际交互确认更新范围,而不是仅凭代码变短就判断迁移成功。

Observation 提供的是更精细的状态追踪机制。稳定的 SwiftUI 架构仍然来自清楚的所有权、窄接口和可替换的副作用边界。