Combine 框架

根据 “Separation of concerns” 关注点分离原则,最好分成视图 View 和视图模型 ViewModel 两部分代码,所以会用到跨文件数据交互,所以有了combine框架。

// 使用前首先要在 swiftUI 文件中,导入 Combine 框架
// 现在好像不需要导入也能用 ?
import Combine

状态发布端

ObservableObject 协议

ObservableObject 是 Combine 框架中的协议。

// 遵循可观测对象协议
class MyModel: ObservableObject {
    @Published var value: Int = 0
}

@Published 属性包裹器

@Published 是与 ObservableObject 协议一起使用的 “属性包裹器”,属性包装器允许向属性、局部变量和函数参数添加行为

@Published@State 的运作方式非常相似:

如果在不运行 Main actor 时修改 @Published 属性会发生什么情况?结果可能是 Xcode 会抛出运行时警告。

  1. 如果需要更多的控制,最好放弃 @Published 并添加自己的属性观察器,再通过 objectWillChange.send() 一起发布
  2. 使用 @Published 会在属性之外创建一个发布者,因此您可以根据需要使用 $ 运算符来链接 Combine 运算符

objectWillChange.send 方法

ObservableObject 类中,当一个属性发生变化需要通知观察视图时,除了使用 @Published 属性包装器进行标记,还有一种方法是手动调用  objectWillChange.send()  方法发送更新通知,这意味着任何观察视图都将重新调用其 body 属性。类似强制刷新。

class UserAuthentication: ObservableObject {
    var username = "Taylor" {
        willSet {
            objectWillChange.send()
        }
    }
}

ObservableObject 对象发出 objectWillChange 通知时,那些使用 @StateObject@ObservedObject@EnvironmentObject 来订阅对象的视图,就会自动更新,类似强制要求观察者刷新。手动调用 objectWillChange.send 的方式可以让开发者更好地控制视图的更新时机。比如只有在满足某些条件时才更新视图。这种方式比单纯使用 @Published 属性包装器更灵活。


objectWillChange 和 @Published 区别

<aside> 💡

在大多数情况下,不需要手动调用 objectWillChange.send 方法。但是,如果您是一次更新多个已发布的属性,那将这些属性标记为常规属性,并改用手动发送信号的方式,可能是更好的解决方案。

</aside>


状态接收端

@StateObject

@StateObject 属性包装器是 @ObservedObject 的一种特殊形式,具有所有相同的功能,但它应该用于创建观察到的对象,而不仅仅是在外部存储传递的对象。

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
            }
        }
    }
}

@ObservedObject

//示例:
struct MyView: View {
    @ObservedObject private var userRegisViewModel = UserRegistrationViewModel()
}

@ObservedObject 的具体使用流程:

  1. 首先,用单独的代表 ViewModel 的文件(符合关注点分离原则),创建遵循 ObservableObject 协议的数据模型(Model),其中包含多个 @Published 标记的属性,以表示可观察的数据;
  2. 然后,在单独的 View 文件中,使用 @ObservedObject 属性包装器实作这个数据模型(Model),这将使视图成为数据模型的订阅者,可以在数据发生变化时得到通知;
  3. 最后,当数据模型的 @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)
}

@EnvironmentObject