1.5 监控应用中的数据变化

原文链接: https://developer.apple.com/documentation/swiftui/monitoring-model-data-changes-in-your-app

1.5 监控应用中的数据变化

使用可观察对象在应用的用户界面中显示数据变化。

概述

你通常会用与用户界面(UI)和其他逻辑相分离的数据模型来存储和处理应用中的数据。这种分离有助于模块化、提升可测试性,也让人更容易理解应用的工作方式。

传统上你会用视图控制器在模型和 UI 之间来回搬运数据,而 SwiftUI 替你处理了大部分同步工作。要在数据变化时更新视图,请让数据模型类成为可观察对象、发布它们的属性,并用属性包装器声明它们的实例。要确保由用户驱动的数据改动流回模型,请把 UI 控件绑定到模型属性上。这些特性协同工作,帮助你为数据维护单一数据源。

重要:本文介绍的是最低部署目标早于 iOS 17、iPadOS 17、macOS 14、tvOS 17 或 watchOS 10 的 SwiftUI 应用如何观察模型数据变化。要让你的应用适配这些操作系统的更新版本,参见从 Observable Object 协议迁移到 Observable 宏。如果你的应用最低部署目标是 iOS 17、iPadOS 17、macOS 14、tvOS 17、watchOS 10 或更高版本,请使用在应用中管理模型数据中所述的观察方式。

让模型数据可观察

要让模型中的数据变化对 SwiftUI 可见,请让模型类采用 ObservableObject 协议。例如,你可以创建一个可观察对象 Book 类:

1
2
class Book: ObservableObject {
}

系统会自动推断该类的 ObjectWillChangePublisher 关联类型,并合成所需的 objectWillChange 方法,由它发出已发布属性的变化值。要发布某个属性,请给该属性的声明加上 Published 属性包装器:

1
2
3
class Book: ObservableObject {
    @Published var title = "Sample Book Title"
}

不需要时,请避免使用已发布属性带来的开销。只发布那些既会变化、又对 UI 有意义的属性。例如,Book 类可能有一个初始化之后永不改变的 identifier 属性:

1
2
3
4
5
class Book: ObservableObject {
    @Published var title = "Sample Book Title"

    let identifier = UUID() // 一个永不改变的唯一标识符。
}

你仍然可以在用户界面中显示该标识符,但由于它没有被发布,SwiftUI 不会监视这个属性的变化。

监控可观察对象的变化

要告诉 SwiftUI 监控某个可观察对象,请给属性声明加上 ObservedObject 属性包装器:

1
2
3
4
5
6
7
struct BookView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        Text(book.title)
    }
}

如上面所示,你可以把可观察对象的单个属性传给子视图。当数据发生变化时——例如你从磁盘加载新数据时——SwiftUI 会更新所有受影响的视图。你也可以把整个可观察对象传给子视图,在视图层级的多层之间共享模型对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct BookView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        BookEditView(book: book)
    }
}

struct BookEditView: View {
    @ObservedObject var book: Book

    // ...
}

在视图中实例化模型对象

SwiftUI 可能随时创建或重建视图,因此用同一组输入初始化视图时、总能得到相同的视图就很重要。因此,在视图内部创建可观察对象并不安全。SwiftUI 提供了 StateObject 属性包装器,它为你在视图层级中存储的引用类型创建单一数据源。你可以这样安全地在视图中创建 Book 实例:

1
2
3
4
5
6
7
struct LibraryView: View {
    @StateObject private var book = Book()
    
    var body: some View {
        BookView(book: book)
    }
}

状态对象的行为与可观察对象类似,区别在于:对某个给定的视图实例,无论 SwiftUI 重建该视图多少次,它都会创建并管理同一个对象实例。你可以在本地使用这个对象,也可以像上面的例子那样,把状态对象传入另一个视图的可观察对象属性。

虽然 SwiftUI 不会在视图内重建状态对象,但它会为每个视图实例创建各自不同的对象实例。例如,下面代码中的每个 LibraryView 都会得到一个独立的 Book 实例:

1
2
3
4
VStack {
    LibraryView()
    LibraryView()
}

你也可以在顶层 App 实例中,或者在应用的某个 Scene 实例中创建状态对象。例如,如果你为某个读书应用定义了一个存放书籍集合的可观察对象 Library,就可以在应用的顶层结构中创建一个图书馆实例:

1
2
3
4
5
6
@main
struct BookReader: App {
    @StateObject private var library = Library()

    // ...
}

在整个应用中共享对象

如果你有一个想在整个应用中使用、但不想层层穿过视图层级传递的数据模型对象,可以改用 environmentObject(_:) 视图修饰符把该对象放入环境:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@main
struct BookReader: App {
    @StateObject private var library = Library()
    
    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(library)
        }
    }
}

应用该修饰符的视图的任何后代视图,都可以通过用 EnvironmentObject 属性包装器声明属性来访问该数据模型实例:

1
2
3
4
5
struct LibraryView: View {
    @EnvironmentObject var library: Library
    
    // ...
}

如果使用环境对象,你可以像上面那样把它加到应用层级最顶部的视图上。你也可以把它加到视图层级中某个子树的根视图上。无论哪种方式,都请记得也把它加进任何使用该对象、或其后代使用该对象的视图的预览提供者中:

1
2
3
4
5
6
struct LibraryView_Previews: PreviewProvider {
    static var previews: some View {
        LibraryView()
            .environmentObject(Library())
    }
}

用绑定建立双向连接

当你允许人们在 UI 中修改数据时,请对相应属性使用绑定。这样可以确保更新自动流回数据模型。你可以在对象名前加上美元符号($),从而取得指向可观察对象、状态对象或环境对象属性的绑定。例如,如果你在 BookEditView 中添加一个 TextField,让人们编辑书籍标题,就给该文本输入框一个指向书籍 title 属性的绑定:

1
2
3
4
5
6
7
struct BookEditView: View {
    @ObservedObject var book: Book
    
    var body: some View {
        TextField("Title", text: $book.title)
    }
}

绑定把视图元素与底层模型连接起来,使人们可以直接修改模型数据。