根据 “Separation of concerns” 关注点分离原则,最好分成 视图View 和 视图模型ViewModel 两部份组织代码,所以就会用到跨文件数据交互,所以有了combine框架。
@Published
// 使用前首先要在 swiftUI 文件中,导入 Combine 框架
// 现在好像不需要导入也能用 ?
import Combine
ObservableObject
是 Combine 框架中的协议。
@EnvironmentObject
@ObservedObject
一起使用,只有 class 可以// 遵循可观测对象协议
class MyModel: ObservableObject {
@Published var value: Int = 0
}
@Published
是与 ObservableObject
协议一起使用的 “属性包裹器”,属性包装器允许向属性、局部变量和函数参数添加行为
ObservableObject
协议,类或结构体需要实现至少一个被 @Published
属性包装器标记的属性@Published
,实际为该属性添加的行为是:在修改此变量时会发送消息,通知所有监听的订阅者,这会导致任何监视其父类的视图重新加载其 body
属性@Published
跟 @State
的运作方式非常相似:
@State
只能用在属于特定/单个 SwiftUI 视图的一个值类型属性ObservableObject
类 + @Published
属性包裹器,则可以不属于特定视图、可以跨视图使用如果在不运行 Main actor
时修改 @Published
属性会发生什么情况?结果可能是 Xcode 会抛出运行时警告。
@Published
并添加自己的属性观察器,再通过 objectWillChange.send()
一起发布@Published
会在属性之外创建一个发布者,因此您可以根据需要使用 $
运算符来链接 Combine 运算符在 ObservableObject
类中,当一个属性发生变化需要通知观察视图时,除了使用 @Published
属性包装器进行标记,还有一种方法是手动调用 objectWillChange.send()
方法发送更新通知,这意味着任何观察视图都将重新调用其 body
属性。类似强制刷新。
class UserAuthentication: ObservableObject {
var username = "Taylor" {
willSet {
objectWillChange.send()
}
}
}
当 ObservableObject
对象发出 objectWillChange
通知时,那些使用 @StateObject
、@ObservedObject
或 @EnvironmentObject
来订阅对象的视图,就会自动更新,类似强制要求观察者刷新。手动调用 objectWillChange.send
的方式可以让开发者更好地控制视图的更新时机。比如只有在满足某些条件时才更新视图。这种方式比单纯使用 @Published
属性包装器更灵活。
@Published
objectWillChange.send()
@Published
,以便您可以根据需要添加额外的Combine operatorsobjectWillChange.send()
对 Core Data relationships 很有用,例如,你可以通知系统刷新父对象,因为您已调整其子值@ObservedObject
关键词可以将某个特定的遵循 ObservableObject
协议的数据模型对象,引入到单个视图中@ObservedObject
适用于在单个 UI 视图监视局部数据。该数据模型通常是该 UI 视图特定的,而不是整个应用程序共享的//示例:
struct MyView: View {
@ObservedObject private var userRegisViewModel = UserRegistrationViewModel()
}
@ObservedObject
的具体使用流程:
ObservableObject
协议的数据模型(Model),其中包含多个 @Published
标记的属性,以表示可观察的数据;@ObservedObject
属性包装器实作这个数据模型(Model),这将使视图成为数据模型的订阅者,可以在数据发生变化时得到通知;@Published
属性发生变化时,使用到该属性的相关视图将自动更新;这种方式能够实现响应式的用户界面,无需手动刷新视图,而是让 SwiftUI 处理一切。这对数据交互密切的应用非常有用,如表单、列表等。// 文件一:ViewModel
import Foundation
import Combine
class UserRegistrationViewModel: ObservableObject {
// 这6个Published都是发布者
// Input 代表用户输入的
@Published var username = ""
@Published var password = ""
@Published var passwordConfirm = ""
// Output 代表系统输出的
@Published var isUsernameLengthValid = false
@Published var isPasswordLengthValid = false
@Published var isPasswordConfirmValid = false
private var cancellableSet: Set<AnyCancellable> = []
// 下面这三段代码,其实是构建上面“代表系统输出”的发布属性,因为他们要计算才能得出
// 否则如果没有上面“代表输出部份”的属性,都不需要下面三段代码
init() {
// 1. 这个 $username 是想要监听的变化值的来源
$username
//呼叫 receive(on:) 函式來確認訂閱者 接收 主执行序列(也就是 RunLoop.main)的值
//这一行指定接下来的操作在主线程上执行。这对于更新 UI 相关的操作很重要,因为 UI 更新必须在主线程上执行
.receive(on: RunLoop.main)
// 2. 这里用map方法作为返回值
.map { username in
return username.count >= 4
}
// 3. 这里将返回值赋值给订阅者(isUsernameLengthValid),Combine 提供了两种订阅者 “assign” 和 “sink”
// 使用 .assign(to:on:) 操作符,将上面的检查结果(true 或 false)分配给名为 isUsernameLengthValid 的属性。
.assign(to: \\.isUsernameLengthValid, on: self)
// store 函式可以儲存 cancellable 的參照(reference)至一個集合(set),作為之後的清除
// 使用 .store(in:) 操作符将 Combine 订阅关系存储在 cancellableSet 中
// 以确保订阅在适当的时候取消,以避免内存泄漏,如果沒有儲存這個參照,App 可能會有記憶體洩漏的問題
.store(in: &cancellableSet)
// 构建第2个发布者的逻辑
$password
.receive(on: RunLoop.main)
.map { password in
return password.count >= 8
}
.assign(to: \\.isPasswordLengthValid, on: self)
.store(in: &cancellableSet)
// 构建第3个发布者的逻辑
// 它是通过两个信息源的值里计算得出的,最后赋予了 isPasswordConfirmValid
Publishers.CombineLatest($password, $passwordConfirm)
.receive(on: RunLoop.main)
.map { (password, passwordConfirm) in
return !passwordConfirm.isEmpty && (passwordConfirm == password)
}
// 发布者isPasswordConfirmValid的值,是通过上面map方法赋予的
.assign(to: \\.isPasswordConfirmValid, on: self)
.store(in: &cancellableSet)
}
}
// 文件二: 视图文件
// 文件这里要实作这个类,否则这个模型不存在
@ObservedObject private var userRegisViewModel = UserRegistrationViewModel()
var body: some View {
TextField("Enter a username", text: $userRegisViewModel.username)
RequirementTextView(icon: "flag.circle.fill", text: "A minimun of 4 characters")
.foregroundColor(userRegisViewModel.usernameLengthValid ? .gray : .red)
}
@StateObject
属性包装器是 @ObservedObject
的一种特殊形式,具有所有相同的功能,但它应该用于创建观察到的对象,而不仅仅是在外部存储传递的对象。
@StateObject
用于在视图中声明一个遵循 ObservableObject
协议的持久对象。当对象属性发生变化时,视图会自动更新@StateObject
对象的生命周期由该视图管理,它在视图生命周期内持久保持。适用于需要在视图生命周期内保持一致的对象ObservableObject
实例,并且希望视图在整个生命周期内都拥有它时,就可以使用 @StateObject
class Counter: ObservableObject {
@Published var count = 0
}
struct CounterView: View {
// 在该视图内实作,并且生命周期和该视图保持一致。视图被销毁,它也会被销毁
@StateObject private var counter = Counter()
var body: some View {
VStack {
Text("Count: \\(counter.count)")
Button("Increment") {
counter.count += 1
}
}
}
}
@EnvironmentObject
可以将某个特定的遵循 ObservableObject
协议的数据模型对象,引入到单个视图中@EnvironmentObject
适用于在整个 SwiftUI 视图层次结构中共享一个数据模型对象,通常是应用程序级别的全局数据。引入后,SwiftUI 会监听它的属性值,有任何改变时,其关联的视图会失效@EnvironmentObject
适用于需要在多个视图之间共享相同数据的情况。通过在应用的顶层视图中实作 environmentObject
,然后可以在整个应用程序中访问和修改这个数据模型@EnvironmentObject
时,整个 App 的全部视图都可以存取它。如果你的 App 有许多视图共享相同的资料,@EnvironmentObject
就很适合,你不需要在视图之间再传递属性,就可以自动存取@ObservedObject
一样,永远不要为 @EnvironmentObject
属性分配值。相反,它应该从其他地方传入,最终您可能会想要使用 @StateObject
在某个地方创建它//示例
struct ContentView: View {
@EnvironmentObject var store : DataStore
}
@EnvironmentObject
的基本使用流程如下:
首先,新建一个可被观察的类,遵循 ObservableObject
协定。在类里加 @Published
关键词的属性,和初始化函数。加上了 @Published
的属性,就是可以被全局传递的数据;同时包含它的 SettingStore 类,会一直监听自身的这些属性,但数据发生变化时,它将通知其它订阅者们新的属性值。
//Swift 文件一:View Model
//final关键字在大多数的编程语言中都存在,表示不允许对其修饰的内容进行继承或者重新操作
//Swift中,final关键字可以在class、func和var前修饰
//通常认为使用final可以更好地对代码进行版本控制,发挥更佳的性能,同时使代码更安全
//新建类,遵循 ObservableObject 协议
final class SettingStore: ObservableObject {
//下面 UpdateItem 是提前定义好的一个 对象结构
@Published var updates : [UpdateItem]
//类需要初始化函数,一般这样写
init(updates: [UpdateItem]) {
self.updates = updates
}
}
然后,到需要用这些 @Published
属性的每个页面,用 @EnvironmentObject
关键词声明一个属于前面模型类别的属性,但不需要实作。然后在具体视图中采用点语法访问需要的属性即可,这个值就是之前模型里加了 @Published 关键词的属性的值。
//文件1
struct DetailView: View {
@EnvironmentObject var myConfi: Configuration
var body: some View {
Text("Detail View")
Text(myConfi.theme)
}
}
//文件2
struct Detail2View: View {
@EnvironmentObject var myConfi: Configuration
var body: some View {
Text("Detail View2")
Text(myConfi.name)
}
}
<aside>
💡 这些文件声明的 @EnvironmentObject
属性,让它会在环境中自动查找一个 Configuration
实例,并且把找到的结果放进 myConfi
属性里。注意:如果环境中找不到实例,你的应用就会崩溃。
</aside>
修复预览:声明完后这时就会报错,因为我们还没有实作模型,所以缺失这个模型对象。解决方法是通过 .environmentObject
修饰符在预览视图加上环境对象,并实作一个赋值;
#Preview {
Detail2View().environmentObject(Configuration())
}
同理,要在项目真正的代码上实作该类。一般来说都是在项目顶级视图(例如contentView)中,实作这个模型对象。这样实际环境中就有对象了,不会报缺失。
struct ContentView: View {
var myConfi: Configuration = Configuration()
var body: some View {
WindowGroup {
NavigationStack{
VStack(spacing: 20) {
Text(myConfi.theme)
// 这里引用了两个子视图。要通过 environmentObject 修饰符,往环境注入上面实作的模型
DetailView().environmentObject(myConfi)
Detail2View().environmentObject(myConfi)
}
}
}
}
}
// 另外一种写法更好:把 myConfi 放到父视图的环境修饰符中
// 因为 DetailView 和 Detail2View 都是 ContentView 的子视图,所以它们自动继承了 ContentView 的环境
// 如果需要向环境中添加多个对象,则应该添加多个 environmentObject() 修饰符
VStack {
DetailView()
Detail2View()
}
.environmentObject(myConfi)
<aside>
💡 你可能会好奇 SwiftUI 是如何在 .environmentObject(user)
和 @EnvironmentObject var user: User
之间建立联系的?它怎么知道要对象正确地塞进哪个属性?是这样的,你已经学习过字典。一个类型存键,另一个类型存值。
环境可以很有效地让我们以数据的类型本身作为键,该类型的实例作为值。这个做法一开始有点费解,你可以这么想:键是 Int
,String
和 Bool
这样的东西,而值是像 5,“Hello” 和 true 这样的东西。也就是说,假如我们讲 “给我 Int
”,那我们会得到 5。
</aside>
<aside>
💡 它们的主要区别是:它们的生命周期和使用场景。您应该使用 @StateObject
在某处创建可观察对象,并且在传递该对象的所有后续位置中应该使用 @ObservedObject
。
</aside>
// 示例
class ViewModel: ObservableObject {
@Published var data: String = "Hello, World!"
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text(viewModel.data)
}
}