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 时是做不到的。
  • 可以使用 State() 和 Environment 等已有的数据流原语,而不必使用 StateObject 和 EnvironmentObject 这类基于对象的等价物。
  • 视图的 body 读取了哪些可观察属性,就只在那些属性变化时更新视图,而不是在任何可观察对象发生属性变化时都更新,这有助于提升应用性能。

为了在你的应用中利用这些好处,你将了解如何把依赖 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()
    }
}