Combine 系列
- Swift Combine 从入门到精通一
- Swift Combine 发布者订阅者操作者 从入门到精通二
- Swift Combine 管道 从入门到精通三
- Swift Combine 发布者publisher的生命周期 从入门到精通四
- Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五
- Swift Combine 订阅者Subscriber的生命周期 从入门到精通六
- Swift 使用 Combine 进行开发 从入门到精通七
- Swift 使用 Combine 管道和线程进行开发 从入门到精通八
- Swift Combine 使用 sink, assign 创建一个订阅者 从入门到精通九
- Swift Combine 使用 dataTaskPublisher 发起网络请求 从入门到精通十
- Swift Combine 用 Future 来封装异步请求 从入门到精通十一
- Swift Combine 有序的异步操作 从入门到精通十二
- Swift Combine 使用 flatMap 和 catch错误处理 从入门到精通十三
- Swift Combine 网络受限时从备用 URL 请求数据 从入门到精通十四
- Swift Combine 通过用户输入更新声明式 UI 从入门到精通十五
- Swift Combine 级联多个 UI 更新,包括网络请求 从入门到精通十六
- Swift Combine 合并多个管道以更新 UI 元素 从入门到精通十七
- Swift Combine 通过包装基于 delegate 的 API 创建重复发布者 从入门到精通十八
- Swift Combine 响应NotificationCenter 的更新 从入门到精通十九
1. 和 SwiftUI 集成 使用 ObservableObject 与 SwiftUI 模型作为发布源
目的: SwiftUI 包含 @ObservedObject
和 ObservableObject
协议,它为 SwiftUI 的视图提供了将状态外部化的手段,同时通知 SwiftUI 模型的变化。
SwiftUI 视图是基于某些已知状态呈现的声明性结构,当该状态发生变化时,这些当前的结构将失效并更新。 我们可以使用 Combine 来提供响应式更新来操纵此状态,并将其暴露回 SwiftUI。 此处提供的示例是一个简单的输入表单,目的是根据对两个字段的输入提供响应式和动态的反馈。
以下规则被编码到 Combine
的管道中:
- 两个字段必须相同 - 如输入密码或电子邮件地址,然后通过第二个条目进行确认。
- 输入的值至少为 5 个字符的长度。
- 根据这些规则的结果启用或禁用提交按钮。
SwiftUI 通过将状态外化为类中的属性,并使用 ObservableObject
协议将该类引用到模型中来实现此目的。 * 两个属性 firstEntry
和 secondEntry
作为字符串使用 @Published
属性包装,允许 SwiftUI
绑定到它们的更新,以及更新它们。
- 第三个属性
submitAllowed
暴露为Combine
发布者,可在视图内使用,从而维护视图内部的@State buttonIsDisabled 状态
。 - 第四个属性 —— 一个
validationMessages
字符串数组 - 在 Combine 管道中将前两个属性进行组合计算,并且使用@Published
属性包装暴露给 SwiftUI。
SwiftUI-Notes/ReactiveFormModel.swift
import Foundation
import Combine
class ReactiveFormModel : ObservableObject {
@Published var firstEntry: String = "" {
didSet {
firstEntryPublisher.send(self.firstEntry) // 1
}
}
private let firstEntryPublisher = CurrentValueSubject<String, Never>("") // 2
@Published var secondEntry: String = "" {
didSet {
secondEntryPublisher.send(self.secondEntry)
}
}
private let secondEntryPublisher = CurrentValueSubject<String, Never>("")
@Published var validationMessages = [String]()
private var cancellableSet: Set<AnyCancellable> = []
var submitAllowed: AnyPublisher<Bool, Never>
init() {
let validationPipeline = Publishers.CombineLatest(firstEntryPublisher, secondEntryPublisher) // 3
.map { (arg) -> [String] in // 4
var diagMsgs = [String]()
let (value, value_repeat) = arg
if !(value_repeat == value) {
diagMsgs.append("Values for fields must match.")
}
if (value.count < 5 || value_repeat.count < 5) {
diagMsgs.append("Please enter values of at least 5 characters.")
}
return diagMsgs
}
submitAllowed = validationPipeline // 5
.map { stringArray in
return stringArray.count < 1
}
.eraseToAnyPublisher()
let _ = validationPipeline // 6
.assign(to: \.validationMessages, on: self)
.store(in: &cancellableSet)
}
}
firstEntry
和secondEntry
都使用空字符串作为默认值。- 然后,这些属性还用
currentValueSubject
进行镜像,该镜像属性使用来自每个@Published
属性的didSet
发送更新事件。这驱动下面定义的Combine
管道,以便在值从 SwiftUI 视图更改时触发响应式更新。 combineLatest
用于合并来自firstEntry
或secondEntry
的更新,以便从任一来源来触发更新。map
接受输入值并使用它们来确定和发布验证过的消息数组。该数据流validationPipeline
是两个后续管道的发布源。- 第一个后续管道使用验证过的消息数组来确定一个
true
或false
的布尔值发布者,用于启用或禁用提交按钮。 - 第二个后续管道接受验证过的消息数组,并更新持有的该 Observed
在这里插入代码片
Object 实例的validationMessages
,以便 SwiftUI 在需要时监听和使用它。
两种不同的状态更新的暴露方法 —— 作为发布者或外部状态,在示例中都进行了展示,以便于你可以更好的利用任一种方法。 提交按钮启用/禁用的选项可作为 @Published
属性进行暴露,验证消息的数组可作为 <String[], Never>
类型的发布者而对外暴露。 如果需要涉及作为显式状态去跟踪用户行为,则通过暴露 @Published
属性可能更清晰、不直接耦合,但任一种机制都是可以使用的。
上述模型与声明式地使用外部状态的 SwiftUI 视图相耦合。
SwiftUI-Notes/ReactiveForm.swift
import SwiftUI
struct ReactiveForm: View {
@ObservedObject var model: ReactiveFormModel // 1
// $model is a ObservedObject<ExampleModel>.Wrapper
// and $model.objectWillChange is a Binding<ObservableObjectPublisher>
@State private var buttonIsDisabled = true // 2
// $buttonIsDisabled is a Binding<Bool>
var body: some View {
VStack {
Text("Reactive Form")
.font(.headline)
Form {
TextField("first entry", text: $model.firstEntry) // 3
.textFieldStyle(RoundedBorderTextFieldStyle())
.lineLimit(1)
.multilineTextAlignment(.center)
.padding()
TextField("second entry", text: $model.secondEntry) // 4
.textFieldStyle(RoundedBorderTextFieldStyle())
.multilineTextAlignment(.center)
.padding()
VStack {
ForEach(model.validationMessages, id: \.self) { msg in // 4
Text(msg)
.foregroundColor(.red)
.font(.callout)
}
}
}
Button(action: {}) {
Text("Submit")
}.disabled(buttonIsDisabled)
.onReceive(model.submitAllowed) { submitAllowed in // 5
self.buttonIsDisabled = !submitAllowed
}
.padding()
.background(RoundedRectangle(cornerRadius: 10)
.stroke(Color.blue, lineWidth: 1)
)
Spacer()
}
}
}
struct ReactiveForm_Previews: PreviewProvider {
static var previews: some View {
ReactiveForm(model: ReactiveFormModel())
}
}
- 数据模型使用
@ObservedObject
暴露给 SwiftUI。 @State
buttonIsDisabled
在该视图中被声明为局部变量,有一个默认值true
。- 属性包装(
$model.firstEntry
和$model.secondEntry
) 的预计值用于将绑定传递到 TextField 视图元素。当用户更改值时,Binding
将触发引用模型上的更新,并让 SwiftUI 的组件知道,如果暴露的模型正在被更改,则组件的更改也即将发生。 - 在数据模型中生成和
assign
的验证消息,作为Combine
管道的发布者,在这儿对于 SwiftUI 是不可见的。相反,这只能对这些被暴露的值的变化所引起的模型的变化做出反应,而不关心改变这些值的机制。 - 作为如何使用带有
onReceive
的发布者的示例,使用onReceive
订阅者来监听引用模型中暴露的发布者。在这个例子中,我们接受值并把它们作为局部变量@State
存储在 SwiftUI 的视图中,但它也可以在一些转化后使用,如果该逻辑只和视图显示的结果值强相关的话。在这,我们将其与Button
上的disabled
一起使用,使 SwiftUI 能够根据@State
中存储的值启用或禁用该 UI 元素。
参考
https://heckj.github.io/swiftui-notes/index_zh-CN.html
代码
https://github.com/heckj/swiftui-notes