Highlight
Swift 5.9 引入
@Observable宏,让数据模型不再需要@Published和ObservableObject,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 时,只需要记住三个问题:
- 这个模型是视图自己的状态吗?是,用
@State。 - 这个模型需要在全局环境共享吗?是,用
@Environment。 - 这个模型只需要绑定吗?是,用
@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由视图创建和管理,用@Stateaccount在多个视图间共享,用.environment()注入,@Environment读取donut从父视图传入,需要双向绑定,用@Bindabledonut只读显示,不需要任何包装器
性能优势
(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 把刷新范围缩小到最小。
核心启发
-
重构现有 SwiftUI 项目的数据层
- 把
ObservableObject+@Published模型迁移到@Observable - 删除视图中大量
@ObservedObject和@EnvironmentObject - 入口:逐个模型替换,从叶子视图开始向上推进
- 把
-
构建高性能的大列表应用
- 列表中每个 cell 对应一个 Observable 模型实例
- 修改单个 cell 的数据时,只有该 cell 刷新
- 入口:
List(items) { ItemRow(item: $0) },ItemRow直接接收Item实例
-
设计可复用的编辑组件
- 用
@Bindable包装模型,组件内部用TextField、Toggle等绑定控件 - 组件不依赖特定的状态管理方式,父视图决定用
@State还是@Environment - 入口:
struct EditorView: View { @Bindable var item: Item }
- 用
-
实现跨视图共享的用户状态
- 用
@Environment在根视图注入用户账户、设置等全局状态 - 子视图按需读取特定属性,只在该属性变化时刷新
- 入口:
.environment(userAccount)+@Environment(Account.self)
- 用
关联 Session
- What’s new in SwiftUI — SwiftUI 2023 全部新特性概览
- Build an app with SwiftData — SwiftData 与 SwiftUI 的实战集成
- Write Swift macros — 深入理解 Swift 宏系统的工作原理
- Demystify SwiftUI performance — SwiftUI 性能优化指南
评论
GitHub Issues · utterances