Skip to content

Latest commit

 

History

History
180 lines (142 loc) · 8.67 KB

pattern-observableobject.adoc

File metadata and controls

180 lines (142 loc) · 8.67 KB

使用 ObservableObject 与 SwiftUI 模型作为发布源

目的
  • SwiftUI 包含 @ObservedObject 和 ObservableObject 协议,它为 SwiftUI 的视图提供了将状态外部化的手段,同时通知 SwiftUI 模型的变化。

参考
另请参阅

SwiftUI 的例子:

代码和解释

SwiftUI 视图是基于某些已知状态呈现的声明性结构,当该状态发生变化时,这些当前的结构将失效并更新。 我们可以使用 Combine 来提供响应式更新来操纵此状态,并将其暴露回 SwiftUI。 此处提供的示例是一个简单的输入表单,目的是根据对两个字段的输入提供响应式和动态的反馈。

以下规则被编码到 Combine 的管道中: 1. 两个字段必须相同 - 如输入密码或电子邮件地址,然后通过第二个条目进行确认。 2. 输入的值至少为 5 个字符的长度。 3. 根据这些规则的结果启用或禁用提交按钮。

SwiftUI 通过将状态外化为类中的属性,并使用 ObservableObject 协议将该类引用到模型中来实现此目的。 两个属性 firstEntrysecondEntry 作为字符串使用 @Published 属性包装,允许 SwiftUI 绑定到它们的更新,以及更新它们。 第三个属性 submitAllowed 暴露为 Combine 发布者,可在视图内使用,从而维护视图内部的 @State buttonIsDisabled 状态。 第四个属性 —— 一个 validationMessages 字符串数组 - 在 Combine 管道中将前两个属性进行组合计算,并且使用 @Published 属性包装暴露给 SwiftUI。

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)
    }
}
  1. firstEntry 和 secondEntry 都使用空字符串作为默认值。

  2. 然后,这些属性还用 currentValueSubject 进行镜像,该镜像属性使用来自每个 @Published 属性的 didSet 发送更新事件。这驱动下面定义的 Combine 管道,以便在值从 SwiftUI 视图更改时触发响应式更新。

  3. combineLatest 用于合并来自 firstEntrysecondEntry 的更新,以便从任一来源来触发更新。

  4. reference.adoc 接受输入值并使用它们来确定和发布验证过的消息数组。该数据流 validationPipeline 是两个后续管道的发布源。

  5. 第一个后续管道使用验证过的消息数组来确定一个 true 或 false 的布尔值发布者,用于启用或禁用提交按钮。

  6. 第二个后续管道接受验证过的消息数组,并更新持有的该 ObservedObject 实例的 validationMessages,以便 SwiftUI 在需要时监听和使用它。

两种不同的状态更新的暴露方法 —— 作为发布者或外部状态,在示例中都进行了展示,以便于你可以更好的利用任一种方法。 提交按钮启用/禁用的选项可作为 @Published 属性进行暴露,验证消息的数组可作为 <String[], Never> 类型的发布者而对外暴露。 如果需要涉及作为显式状态去跟踪用户行为,则通过暴露 @Published 属性可能更清晰、不直接耦合,但任一种机制都是可以使用的。

上述模型与声明式地使用外部状态的 SwiftUI 视图相耦合。

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)
                    .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())
    }
}
  1. 数据模型使用 @ObservedObject 暴露给 SwiftUI。

  2. @State buttonIsDisabled 在该视图中被声明为局部变量,有一个默认值 true

  3. 属性包装($model.firstEntry$model.secondEntry) 的预计值用于将绑定传递到 TextField 视图元素。当用户更改值时,Binding 将触发引用模型上的更新,并让 SwiftUI 的组件知道,如果暴露的模型正在被更改,则组件的更改也即将发生。

  4. 在数据模型中生成和 assign 的验证消息,作为 Combine 管道的发布者,在这儿对于 SwiftUI 是不可见的。相反,这只能对这些被暴露的值的变化所引起的模型的变化做出反应,而不关心改变这些值的机制。

  5. 作为如何使用带有 onReceive 的发布者的示例,使用 onReceive 订阅者来监听引用模型中暴露的发布者。在这个例子中,我们接受值并把它们作为局部变量 @State 存储在 SwiftUI 的视图中,但它也可以在一些转化后使用,如果该逻辑只和视图显示的结果值强相关的话。在这,我们将其与 Button 上的 disabled 一起使用,使 SwiftUI 能够根据 @State 中存储的值启用或禁用该 UI 元素。