第11章:了解属性封装器¶
在你的SwiftUI
应用程序中,每一个可以改变的数据值或对象都需要一个单一的真相来源,以及一个机制来使视图改变或观察它。SwiftUI
的属性包装器使你能够声明每个视图如何与可变数据进行交互。
在本章中,你将回顾你是如何用@State
、@Binding
、@Environment
、ObservableObject
、@StateObject
和@EnvironmentObject
管理HIITFit
中的数据值和对象的。而且,你将建立一个简单的应用程序,让你专注于如何使用这些属性包装器。你还将学习TextField
、environment
修改器和@ObservedObject
属性封装器。
为了帮助回答"结构还是类"的问题,你将看到为什么HistoryStore
应该是一个类,而不是一个结构,并了解SwiftUI
应用程序的自然架构:Model-View-ViewModel (MVVM)
。
开始工作¶
➤ 打开启动器文件夹中的TIL
项目。项目名称TIL
是"今天我学到了"的首字母缩写。或者,你可以把它想象成"我学到的东西"。下面是这个应用程序应该如何工作。用户点击+
按钮来添加缩写词,如YOLO
和BTW
,主屏幕就会显示这些。
这个应用程序将一个VStack
嵌入到一个NavigationView
中。这给了你一个导航栏,在那里你可以显示标题和+
按钮。你将在第3节了解更多关于NavigationView
的信息。
这个项目有一个ThingStore
,像HIITFit
中的HistoryStore
。这个应用比HIITFit
简单得多,所以你可以专注于如何管理数据。
记住你如何管理HIITFit
中HistoryStore
的变化:
在第6章"为你的应用程序添加功能"中,你把HistoryStore
从一个结构转换为符合ObservableObject
的类,然后把它设置为@EnvironmentObject
,这样ExerciseView
和HistoryView
就可以直接访问它。HistoryView
是WelcomeView
的一个子视图,但是你看到了使用@EnvironmentObject
可以避免将HistoryStore
传递给WelcomeView
,后者并不使用它。如果你做了那一章的挑战,你也用@State
和@Binding
来管理HistoryStore
。
在第9章"保存历史数据"中,你把HistoryStore
的初始化从ContentView
移到HIITFitApp
,以便在有或没有保存历史数据的情况下初始化它。
ThingStore
有属性things
,它是一个String
值的数组。和HIITFit第一个版本中的HistoryStore
一样,它是一个结构。
在这一章中,你将首先使用@State
和@Binding
来管理ThingStore
结构的变化,然后将其转换为ObservableObject
类并使用@StateObject
和@ObservedObject
来管理变化:
你会了解到,这两种方法非常相似。
Note
我们的教程Property Wrappers
(bit.ly/3vLOpbl)扩展了这个项目,以使用ThingStore
作为@EnvironmentObject
。
管理数据的工具¶
你已经知道,@State
属性是一个真相的来源。拥有@State
属性的视图可以将其值或其绑定传递给它的子视图。如果它向子视图传递了一个绑定,该子视图现在就有一个对真理之源的引用。这允许它更新该属性的值或在该值改变时重新绘制自己。当一个@State
值发生变化时,任何对其有引用的视图都会使其外观失效,并重新绘制自身以显示新的状态。
你的应用程序需要管理两种数据的变化:
- 用户界面值,如显示或隐藏视图的布尔标志、文本字段文本、滑块或挑选器值。
- 数据模型对象,通常是为应用程序的数据建模的对象集合,如完成练习的每日日志。
属性包装器¶
属性包装器将一个值或对象包装在一个有两个属性的结构中:
wrappedValue
是基础值或对象。projectedValue
是对包裹值的绑定,或者是对对象的投影,可以创建对其属性的绑定。
Swift
语法让你只写属性的名字,比如showHistory
,而不是showHistory.wrappedValue
。而且,它的绑定是$showHistory
而不是showHistory.projectedValue
。
SwiftUI
提供了一些工具--主要是属性包装器--来创建和修改数值和对象的单一真理源:
-
用户界面值:使用
@State
和@Binding
来显示影响视图外观的值,例如showHistory
。底层类型必须是一个值类型,如Bool
、Int
、String
或Exercise
。使用@State
在一个视图中创建一个真相来源,然后将@Binding
传递给子视图的这个属性。一个视图可以作为@Environment
属性或使用environment(_:_:)
视图修改器访问内置的@Environment
值。 -
数据模型对象:对于像
HistoryStore
这样为你的应用程序的数据建模的对象,使用@StateObject
与@ObservedObject
或environmentObject(_:)
与@EnvironmentObject
。底层对象类型必须是符合ObservableObject
的引用类型--一个类,并且它应该至少发布一个值。然后,要么使用@StateObject
和@ObservedObject
,要么声明一个@EnvironmentObject
,其类型与environmentObject(_:)
视图修改器创建的环境对象相同。
在对你的应用程序进行原型设计时,你可以用结构来模拟你的数据,并使用@State
和@Binding
。当你弄清楚数据需要如何在你的应用程序中流动时,你可以重构你的应用程序以适应需要符合ObservableObject
的数据类型。
这就是你在本章要做的,以巩固你对如何使用这些属性包装器的理解。
保存/存在的应用程序或场景状态¶
还有两个你用过的属性包装器。@AppStorage
包装了UserDefault
值。在第8章"保存设置"中,你使用@AppStorage
来保存UserDefaults
中的练习评级,并在应用程序启动时加载它们。
在同一章中,你用@SceneStorage
来保存和恢复场景的状态--iPad
模拟器中的窗口,每个窗口显示不同的练习。
管理UI状态值¶
@State
和@Binding
值属性主要用于管理你的应用程序的用户界面的状态。
视图是一个结构,所以你不能改变一个属性值,除非你把它包装成一个@State
或@Binding
属性。
拥有@State
属性的视图负责初始化它。@State
属性包装器在视图结构之外为该值创建持久性存储,并在视图重绘时保留其值。这意味着初始化只发生一次。
在第6章"为你的应用程序添加功能"中,你已经有很多关于@State
和@Binding
的练习了:
selectedTab
控制TabView
。showHistory
,showSuccess
,showTimer
,timerDone
显示或隐藏视图。rating
和timeRemaining
值必须能够改变。
在该章的挑战中,你用@State
和@Binding
来管理HistoryStore
的变化。这只是一个证明它是可能的练习,这也是你可以采取的一种原型设计方法。对于大多数应用程序,你的最终数据模型将涉及ObservableObject
类。
用@State
和@Binding
来管理ThingStore
。¶
TIL
是一个非常简单的应用程序,这使得我们很容易研究管理应用程序数据的不同方法。首先,你会像在你的应用程序的视图之间共享任何其他可变的值一样,管理ThingStore
。
➤ 在ContentView.swift
中,运行实时预览并点击+
按钮:
TIL
使用一个布尔标志,showAddThing
,来显示或隐藏AddThingView
。这是一个@State
属性,因为当你点击+
按钮时,它的值会发生变化,而且ContentView
拥有它。
➤ 在ContentView.swift
中,给ContentView
添加这一行:
@State private var myThings = ThingStore()
你将向myThings.things
添加项目,所以myThings
必须是一个封装的属性。在这种情况下,它是@State
,因为ContentView
拥有它并初始化了它。
➤ 现在,删除临时数组:
let tempThings = ["YOLO", "BTW"] // delete this line
你将在myThings.things
中存储字符串,所以你不再需要这个数组。
➤ 然后,更新ForEach
参数:
ForEach(myThings.things, id: \.self) { thing in
你在things
数组上循环,而不是tempThings
。
➤ 刷新预览:
现在,没有什么可显示的,因为myThings
初始化时是一个空的things
数组。如果你在用户第一次启动你的应用程序时显示一条信息,而不是这个空白页面,那么用户体验会更好。
➤ 在ContentView.swift
中,在VStack
的顶部,在ForEach
行之前添加这段代码:
if myThings.things.isEmpty {
Text("Add acronyms you learn")
.foregroundColor(.gray)
}
你给你的用户一个提示,告诉他们可以用你的应用程序做什么。文本是灰色的,所以他们知道这只是一个占位符,直到他们添加自己的数据。
AddThingView
需要修改myThings
,所以在AddThingView
中需要一个@Binding
。
➤ 在AddThingView.swift中,给AddThingView
添加这个属性:
@Binding var someThings: ThingStore
你很快就会从ContentView
中传递这个绑定。
➤ 你还会添加一个文本字段,但现在,只是为了在你点击Done
时发生一些事情,在解散这个工作表之前,向按钮动作添加这一行:
someThings.things.append("FOMO")
你把一个特定的字符串追加到数组中。
➤ 修复此视图的 previews
:
AddThingView(someThings: .constant(ThingStore()))
你为ThingStore
的恒定初始值创建一个绑定。
➤ 现在,回到ContentView.swift
中,修复对AddThingView()
的调用:
AddThingView(someThings: $myThings)
你给ContentView``@State
属性传递一个绑定给AddThingView
。
Note
传递一个绑定给子视图对ThingStore
中的所有内容的写入权限。在这种情况下,ThingStore
只有things
数组,但是,如果它有更多的属性,并且你想限制对其things
数组的写访问,你可以传递$myThings.things
--只对things
数组进行绑定。你需要为AddThingView
的预览初始化一个String
数组。
➤ 开始实时预览,点+
然后点Done
:
很好,你已经通过ThingStore
使数据从AddThingView
流向了ContentView
!
现在为了从用户那里获得输入,你将在AddThingView
中添加一个TextField
。
➤ 首先,钉住ContentView
的预览,这样当你准备测试你的TextField
时它就在那里。
使用一个TextField
¶
许多UI控件通过绑定一个参数到视图的@State
属性来工作。这些控件包括Slider
, Toggle
, Picker
和TextField
。
为了通过TextField
获得用户输入,你需要一个可变的String
属性来存储用户的输入。
➤ 在AddThingView.swift
中,将这个属性添加到AddThingView
中:
@State private var thing = ""
这是一个@State
属性,因为它必须在视图重绘时持续存在。AddThingView
拥有这个属性,所以它负责初始化thing
。你把它初始化为空字符串。
➤ 现在,在VStack
中添加你的TextField
,在Done
按钮的上方:
TextField("Thing I Learned", text: $thing) // 1
.textFieldStyle(RoundedBorderTextFieldStyle()) // 2
.padding() // 3
- 标签"我学到的东西"是占位符文本。它在
TextField
中显示为灰色,作为对用户的提示。你给thing
传递一个绑定,所以TextField
可以将这个值设置为用户输入的内容。 - 你用一个圆形的边框来装扮这个
TextField
。 - 添加
padding
,这样从视图的顶部到按钮就有了一些空间。
➤ 然后,编辑按钮动作附加的内容:
if !thing.isEmpty {
someThings.things.append(thing)
}
而不是"FOMO"
,你将用户的文本输入附加到你的things
数组中,然后检查它不是空字符串。
➤ 在ContentView
预览中刷新实时预览,然后点击+。在文本字段中输入一个缩写词,如YOLO
。它会自动将第一个字母大写,但你必须按住Shift
键来输入其余的字母。点Done
:
ContentView
显示你的新首字母缩写。
有时,应用程序会自动纠正你的缩写。FTW
改为GET
,FOMO
改为DINO
。
➤ 将此修改器添加到TextField
:
.disableAutocorrection(true)
访问环境值¶
视图可以访问许多环境值,如accessibilityEnabled
、colorScheme
、lineSpacing
、font
和presentationMode
。苹果的SwiftUI
文档在apple.co/37cOxak中列出了环境值的完整清单。
视图的环境是一种继承机制。视图从其祖先视图继承环境值,其子视图继承其环境值。
➤ 要看到这一点,请打开ContentView.swift
并点击这一行的任何地方:
Text("Add acronyms you learn")
➤ 现在,打开属性检查器:
字体、重量、行数限制、填充和框架大小是继承的。如果你没有将字体颜色设置为灰色,也会被继承。
视图可以重写一个继承的环境值。为一个堆栈设置默认字体,然后为堆栈的子视图中的文本覆盖它是很常见的。你在第3章"主视图的原型设计"中就是这样做的,当时你让第一个页码比其他页码大:
HStack {
Image(systemName: "1.circle")
.font(.largeTitle)
Image(systemName: "2.circle")
Image(systemName: "3.circle")
Image(systemName: "4.circle")
}
.font(.title2)
修改环境值¶
AddThingView
已经使用了presentationMode
环境值,与HIITFit的SuccessView
一样被声明为视图属性。但是,你也可以通过修改视图来设置环境值。
首字母缩写应该以大写字母出现,但很容易忘记按住Shift
键。实际上,你可以设置一个环境值来自动将文本转换为大写。
➤ 在TILApp.swift
中,将此修改器添加到 ContentView()
:
.environment(\.textCase, .uppercase)
你将uppercase
设置为ContentView
及其所有子视图的textCase
的默认值。
Note
textCase(.uppercase)
也可以,但.environment
语法突出了textCase
是一个环境值的事实。
➤ 要在实时预览中看到它,还要在ContentView.swift
中向previews
中的ContentView()
添加这个修改器。
➤ 刷新实时预览,添加缩略语,不必费力地保持所有字母的大写。只需输入yolo
或fomo
。点选Done
。注意这个标签和占位符文本现在都是大写的:
Note
如果占位符文本不全是大写字母,请按Shift-Command-K
来清理构建文件夹。
你的字符串会自动转换为大写字母。
这个环境值适用于你的应用程序中的所有文本,这看起来有点奇怪。没问题 - 你可以覆盖它。
➤ 在AddThingView.swift
中,将这个修改器添加到VStack
中:
.environment(\.textCase, nil)
你把这个值设置为nil
,所以这个VStack
所显示的文本都不会被转换为大写字母。
➤ 刷新实时预览,点击+
,输入icymi
然后点击完成:
现在,按钮标签和占位符文本又恢复了正常。在主屏幕上,uppercase
环境默认值仍然将你的字符串转换为全大写。
管理模型数据对象¶
@State
, @Binding
和 @Environment
只对值数据类型起作用。简单的内置数据类型如Int
, Bool
或String
对于定义你的应用程序的用户界面的状态很有用。
你可以使用自定义的值数据类型,如struct
或enum
来模拟你的应用程序的数据。而且,你可以使用@State
和@Binding
来管理这些值的更新,正如你在本章前面所做的那样。
大多数应用程序也使用类来为数据建模。SwiftUI
提供了一种不同的机制来管理类对象的变化。ObservableObject
、@StateObject
、@ObservedObject
和@EnvironmentObject
。为了练习使用@ObservedObject
,你将重构TIL以使用@StateObject
和@ObservedObject
来更新ThingStore
,它符合ObservableObject
。你会看到与使用@State
和@Binding
有很多相似之处,也有一些区别。
Note
你可以把一个类对象包装成@State
属性,但它的"值"是它在内存中的地址,所以只有当它的地址发生变化时--例如,当应用程序重新初始化时,依赖性视图才会重新绘制自己。
类和结构¶
但是,这一节并不只是为了练习管理对象。ThingStore
实际上应该是一个类,而不是一个结构。
@State
和@Binding
工作得很好,从AddThingView
更新ContentView
中的ThingStore
真值来源。但是ThingStore
并不是结构的最自然的使用。对于你的应用程序使用ThingStore
的方式,一个类是更合适的。
当你需要像HistoryStore
或ThingStore
那样的共享的可改变的状态时,类更适合。当你需要多个独立的状态时,结构更适合,如ExerciseDay
结构。
对于一个类对象,变化是正常的。一个类对象期望其属性发生变化。对于一个结构实例来说,变化是例外的。一个结构实例需要提前通知,一个方法可能会改变一个属性。
一个类对象期望被共享,任何引用都可以被用来改变它的属性。一个结构实例允许自己被复制,但其副本的变化是独立于它和彼此的。
你会在第15章"结构、类和协议 "中了解到更多关于类和结构的信息。
用StateObject
和ObservedObject
管理ThingStore
¶
你已经在第6章"为你的应用程序添加功能"中使用了@EnvironmentObject
,以避免将HistoryStore
通过WelcomeView
到达HistoryView
。
为了把它用作@EnvironmentObject
,你把HistoryStore
从一个结构转换为符合ObservableObject
的类。这也是你在使用@StateObject
和@ObservedObject
与ThingStore
之前的第一步。一旦完成,你将把它作为一个@StateObject
来创建,并把它传递给一个子视图,把它作为一个@ObservedObject
来使用。听起来很像"创建一个@State
属性并传递它的@Binding
",不是吗?
Note
你可以把@State
值或@StateObject
作为@Binding
或@ObservedObject
属性传递给子视图,即使该子视图只需要读取访问。这使得子视图能够在@State
值或ObservableObject
改变时重新绘制自己。在第6章"为你的应用程序添加功能"中,你在HeaderView
中的selectedTab
做了这个。
➤ 在ContentView.swift
中,将ThingStore
结构替换为以下内容:
final class ThingStore: ObservableObject {
@Published var things: [String] = []
}
就像你对HistoryStore
所做的那样,你让ThingStore
成为一个类而不是一个结构,然后让它符合ObservableObject
。你把这个类标记为final
,告诉编译器它不需要检查任何重写属性或方法的子类。
像HistoryStore
一样,ThingStore
发布它的数据阵列。视图通过声明为@StateObject
、@ObservedObject
或@EnvironmentObject
来订阅这个发布者。things
的任何变化都会通知订阅者的视图来重新绘制自己。
你在第7章"观察对象"中使用了@EnvironmentObject
。在HIITFit
中,ExerciseView
和HistoryView
声明对HistoryStore
对象的依赖:
@EnvironmentObject var history: HistoryStore
如果一个视图使用@EnvironmentObject
,你必须通过在祖先视图上调用environmentObject(_:)
修改器来创建模型对象。你首先在ContentView
中创建了HistoryStore
对象,将修改器应用于TabView
:
TabView(selection: $selectedTab) {
...
}
.environmentObject(HistoryStore())
然后,在第9章"保存历史数据"中,你把它的初始化提升了一个层次到HIITFitApp
,并把它声明为@StateObject
。
Note
在environmentObject
修改器中初始化HistoryStore
在你进行原型设计时是有效的。为了确保应用程序永远不会重新初始化环境对象,将其声明并初始化为@StateObject
,然后在environmentObject
修改器中传递属性。
在TIL
中,AddThingView
将使用@ObservedObject
,所以你必须在祖先视图中把模型对象实例化为@StateObject
,然后把它作为参数传递给其子视图。拥有的视图会精确地创建一次@StateObject
。
➤ 在ContentView
中,用这一行替换@State private var myThings = ThingStore()
:
@StateObject private var myThings = ThingStore()
ThingStore
现在是一个类,而不是一个结构,所以你不能使用@State
属性包装器。相反,你可以使用@StateObject
。
@StateObject
属性包装器确保myThings
只被实例化一次。当ContentView
重新绘制自己时,它将持续存在。
➤ 在对AddThingView(someThings:)
的调用中,删除绑定符号$
:
AddThingView(someThings: myThings)
你不需要为myThings
创建一个引用。作为一个类对象,它已经是一个引用。
➤ 在AddThingView.swift
中,将AddThingView
中的@Binding
替换为@ObservedObject
:
@ObservedObject var someThings: ThingStore
Note
如果ThingStore
有更多的属性,并且你想限制对其things
数组的写入权限,你可以将$myThings.things
传递给AddThingView
,它将有一个@Binding someThings: [String]
属性。
➤ 并修复它的previews
:
AddThingView(someThings: ThingStore())
参数不再是一个绑定。
➤ 刷新实时预览,点击+
,输入yolo
然后点击完成:
没有惊喜:这款应用的工作方式仍然和以前一样。
MVVM¶
你可能对其他场合的应用程序的Model-View-Controller (MVC)
架构很熟悉,比如Web
应用程序。你的数据模型对你的应用程序如何向用户展示它一无所知。视图并不拥有数据,而控制器则在模型和视图之间进行调解。
SwiftUI
应用程序的一个常用架构是Model-View-View Model (MVVM)
。没有控制器,所以视图模型为视图准备了模型数据来显示。
视图模型的属性可以包括一个文本字段的当前文本或一个特定按钮是否被启用。在一个视图模型中,你还可以指定视图可以执行的动作,比如点击按钮或手势。
一个用户动作是一个触发视图模型更新模型的事件。如果模型连接到后端数据库,数据可以独立于用户动作而变化。视图模型使用这些来更新视图的状态。
当一个视图显示一个对象或值的集合时,它的视图模型会管理数据的收集。在HIITFit
和TIL
这样的简单应用中,这是视图模型的唯一工作。所以,视图模型的名字中经常包括Store
一词。
HIITFit
的视图模型,HistoryStore
,保存和加载用户的运动历史。该模型由Exercise
和ExerciseDay
结构组成。HistoryStore
发布了exerciseDays
数组。ExerciseView
和HistoryView
订阅HistoryStore
。ExerciseView
的tap-Done
事件更新exerciseDays
数组,从而改变HistoryView
的状态。
TIL
的视图模型,ThingStore
,保存了用户的缩写数组。该模型只是一个String
,视图模型发布了things
数组。ContentView
和AddThingView
订阅ThingStore
。AddThingView
的tap-Done事件更新了things
数组,从而改变了ContentView
的状态。
在第3节的应用程序,RWFreeView
中,视图模型存储了一个Episode
实例的集合。它负责从raywenderlich.com
下载数据,并将数据解码为Episode
。
包裹属性包装器¶
这里有一个总结,可以帮助你对属性包装器进行思考。
首先,决定你是在管理一个值的状态还是一个对象的状态。值主要是用来描述你的应用程序的用户界面的状态。如果你能用值数据类型来模拟你的应用程序的数据,你就很幸运了,因为你有更多的属性封装器选项来处理值。但在某种程度上,大多数应用程序都需要引用类型来为其数据建模,通常是为了从一个集合中添加或删除项目。
值包装¶
@State
和@Binding
是值属性包装器的主力。如果一个视图没有从任何父级视图那里收到该值,那么它就拥有该值。在这种情况下,它是一个@State
属性--唯一的真理来源。当一个视图第一次被创建时,它会初始化其@State
属性。当一个@State
值发生变化时,视图就会重绘自己,重设除@State
属性以外的一切。
所有的视图可以将@State
值作为普通的只读值或作为读写的@Binding
传递给子视图。
当你在制作应用程序的原型并尝试使用子视图时,你可能会把它写成一个只有@State
属性的独立的视图。后来,当你把它放到你的应用程序中时,你只需把@State
改为@Binding
,就可以得到来自父视图的值。
你的应用程序可以访问内置的@Environment
值。一个环境值在你所附加的视图的子树中持续存在。通常,这只是一个像VStack
的容器,你使用环境值来设置默认值,如字体大小。
Note
你也可以定义你自己的环境值,例如,将一个视图的属性暴露给祖先的视图。这超出了本书的范围,但请查阅《SwiftUI教程》第9章"状态与数据流--第二部分"(bit.ly/39bz5vv)。
你可以在@AppStorage
或@SceneStorage
字典中存储一些值。@AppStorage
值在UserDefaults
中,所以它们在应用程序关闭后会持续存在。你使用@SceneStorage
值来恢复应用程序重新打开时的场景状态。在iOS
环境下,场景最容易被看作是iPad
上的多个窗口。
包裹对象¶
当你的应用程序需要改变和响应参考类型的变化时,你创建一个符合ObservableObject
的类,并发布相应的属性。在这种情况下,你使用@StateObject
和@ObservedObject
的方式与@State
和@Binding
的值相同。你在一个视图中把你的发布者类实例化为@StateObject
,然后把它作为@ObservedObject
传递给子视图。当拥有的视图重绘自己时,它不会重置其@StateObject
属性。
如果你的应用程序的视图需要更灵活地访问该对象,你可以把它提升到视图子树的环境中,仍然作为一个@StateObject
。你必须在这里将其实例化。如果你忘记创建它,你的应用程序会崩溃。然后你使用.environmentObject(_:)
修改器将其附加到一个视图上。视图子树中的任何视图都可以通过声明该类型的@EnvironmentObject
来订阅该发布者对象。
要使你的应用程序中的每个视图都能使用环境对象,在App
创建其WindowGroup
时将其附加到根视图。
关键点¶
- 每一个可以改变的数据值或对象都需要一个单一的真理源和一个机制来使视图更新它。
- 使用
@State
和@Binding
来管理用户界面值的变化。 - 作为
@Environment
视图属性或通过使用environment
视图修改器访问@Environment
值。 - 使用
@StateObject
和@ObservedObject
来管理数据模型对象的变化。对象类型必须符合ObservableObject
,并且应该至少发布一个值。 - 如果只有几个子视图需要访问
ObservableObject
,可以将其实例化为@StateObject
,然后在environmentObject
视图修改器中传递它。在任何需要访问它的子视图中声明一个@EnvironmentObject
属性。 - 当对你的应用程序进行原型设计时,你可以使用
@State
和@Binding
来模拟你应用程序的数据结构。当你确定了数据需要如何在你的应用程序中流动时,你可以重构你的应用程序以适应需要符合ObservableObject
的数据类型。 SwiftUI
应用程序的一个常用架构是Model-View-View Model (MVVM)
,其中视图模型是一个ObservableObject
。对视图模型发布的属性的改变会导致模型和视图的更新。