1.4 从 Observable Object 协议迁移到 Observable 宏
原文链接: https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro
1.4 从 Observable Object 协议迁移到 Observable 宏
更新你已有的应用,以利用 Swift 中 Observation 的好处。
概述
从 iOS 17、iPadOS 17、macOS 14、tvOS 17 和 watchOS 10 开始,SwiftUI 支持 Observation——观察者设计模式在 Swift 中的专用实现。采用 Observation 会给你的应用带来下列好处:
为了在你的应用中利用这些好处,你将了解如何把依赖 ObservableObject 的现有源代码,替换为利用 Observable() 宏的代码。
注意:下载这个示例可以看到迁移后的示例应用版本。要查看迁移前的版本,请下载监控应用中的数据变化中提供的示例。你也可以用迁移前的版本跟着本文一起动手。
使用 Observable 宏
要在已有应用中采用 Observation,首先把数据模型类型中的 ObservableObject 替换为 Observable() 宏。Observable() 宏会在编译期生成源代码,为该类型添加观察支持。
1
2
3
4
5
6
| // 迁移前
import SwiftUI
class Library: ObservableObject {
// ...
}
|
1
2
3
4
5
6
| // 迁移后
import SwiftUI
@Observable class Library {
// ...
}
|
然后从可观察属性上移除 Published 属性包装器。Observation 不需要属性包装器就能让属性可观察。相反,属性相对于观察者(例如某个视图)的可访问性,决定了该属性是否可观察。
1
2
3
4
| // 迁移前
class Library {
@Published var books: [Book] = [Book(), Book(), Book()]
}
|
1
2
3
4
| // 迁移后
@Observable class Library {
var books: [Book] = [Book(), Book(), Book()]
}
|
如果你有一些对观察者可见、但你不希望被跟踪的属性,请对它们应用 ObservationIgnored() 宏。
逐步迁移
你不需要在整个应用里成批替换 ObservableObject 协议,可以逐步修改。先从一个数据模型类型开始改用 Observable() 宏。应用可以混用使用不同观察系统的数据模型类型。不过,SwiftUI 依据数据模型类型所使用的观察系统(Observable 还是 ObservableObject)以不同方式跟踪变化。
你可能会注意到,应用会因为跟踪方式不同而出现细微的行为差异。例如,以 Observable() 方式跟踪时,只有当某个可观察属性发生变化、且视图的 body 直接读取该属性时,SwiftUI 才会更新该视图。如果变化的可观察属性没有被 body 读取,视图不会更新。相反,以 ObservableObject 方式跟踪时,只要 ObservableObject 实例的任何已发布属性发生变化,视图就会更新,即使该视图并不读取这个发生变化的属性。
注意:要了解更多关于可观察属性变化时 SwiftUI 何时更新视图的内容,参见在应用中管理模型数据。
迁移其他源代码
到目前为止,对示例应用所做的唯一改动是把 Observable() 宏应用到 Library,并移除对 ObservableObject 协议的支持。应用仍然使用 StateObject 这类 ObservableObject 数据流原语来管理 Library 的实例。如果此时构建并运行应用,SwiftUI 仍然会如预期更新视图。这是因为 StateObject 和 EnvironmentObject 这类数据流属性包装器支持使用 Observable() 宏的类型。SwiftUI 提供这种支持,是为了让应用能够逐步修改源代码。
不过,要完全采用 Observation,请在更新数据模型类型之后,把 StateObject 的用法替换为 State()。例如,在下面这段代码中,主应用结构创建 Library 实例并把它存为 StateObject,还用 environmentObject(_:) 修饰符把 Library 实例加入环境。
1
2
3
4
5
6
7
8
9
10
11
12
| // 迁移前
@main
struct BookReaderApp: App {
@StateObject private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
|
既然 Library 已不再遵循 ObservableObject,代码就可以改用 State() 而不是 StateObject,并改用 environment(_:) 修饰符把 library 加入环境。
1
2
3
4
5
6
7
8
9
10
11
12
| // 迁移后
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(library)
}
}
}
|
在 Library 完全采用 Observation 之前,还需要再做一处改动。此前视图 LibraryView 用 EnvironmentObject 属性包装器从环境中取出 Library 实例,而新代码改用 Environment 属性包装器。
1
2
3
4
5
6
7
8
9
10
| // 迁移前
struct LibraryView: View {
@EnvironmentObject var library: Library
var body: some View {
List(library.books) { book in
BookView(book: book)
}
}
}
|
1
2
3
4
5
6
7
8
9
10
| // 迁移后
struct LibraryView: View {
@Environment(Library.self) private var library
var body: some View {
List(library.books) { book in
BookView(book: book)
}
}
}
|
移除 ObservedObject 属性包装器
为完成示例应用的迁移,请修改数据模型类型 Book 以支持 Observation:从类型声明中移除 ObservableObject,并应用 Observable() 宏。然后从可观察属性上移除 Published 属性包装器。
1
2
3
4
5
6
| // 迁移前
class Book: ObservableObject, Identifiable {
@Published var title = "Sample Book Title"
let id = UUID() // 一个永不改变的唯一标识符。
}
|
1
2
3
4
5
6
| // 迁移后
@Observable class Book: Identifiable {
var title = "Sample Book Title"
let id = UUID() // 一个永不改变的唯一标识符。
}
|
接下来,从 BookView 的 book 变量上移除 ObservedObject 属性包装器。采用 Observation 时并不需要这个属性包装器。因为 SwiftUI 会自动跟踪视图的 body 直接读取的任何可观察属性。例如,当 book.title 变化时,SwiftUI 会更新 BookView。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 迁移前
struct BookView: View {
@ObservedObject var book: Book
@State private var isEditorPresented = false
var body: some View {
HStack {
Text(book.title)
Spacer()
Button("Edit") {
isEditorPresented = true
}
}
.sheet(isPresented: $isEditorPresented) {
BookEditView(book: book)
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 迁移后
struct BookView: View {
var book: Book
@State private var isEditorPresented = false
var body: some View {
HStack {
Text(book.title)
Spacer()
Button("Edit") {
isEditorPresented = true
}
}
.sheet(isPresented: $isEditorPresented) {
BookEditView(book: book)
}
}
}
|
不过,如果视图需要指向某个可观察类型的绑定,请把 ObservedObject 替换为 Bindable 属性包装器。这个属性包装器为可观察类型提供绑定支持,使需要绑定的视图可以修改可观察属性。例如,在下面这段代码中,TextField 接收指向 book.title 的绑定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 迁移前
struct BookEditView: View {
@ObservedObject var book: Book
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack() {
TextField("Title", text: $book.title)
.textFieldStyle(.roundedBorder)
.onSubmit {
dismiss()
}
Button("Close") {
dismiss()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 迁移后
struct BookEditView: View {
@Bindable var book: Book
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack() {
TextField("Title", text: $book.title)
.textFieldStyle(.roundedBorder)
.onSubmit {
dismiss()
}
Button("Close") {
dismiss()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
|