【问题标题】:SwiftUI validation and vetoing for user inputSwiftUI 验证和否决用户输入
【发布时间】:2023-09-21 20:45:01
【问题描述】:

我希望在 SwiftUI 中实现一个通用的验证/否决循环 - 使用“单一事实来源”框架应该非常简单的事情

总之我想:

  • 拥有一个通用控件(比如TextField
  • 对该控件的更新应用验证/否决(例如,用户键入文本)
  • 将预期的更改传播到验证器,在某处更新 Binding 源对象(理想情况下,View 内的 @State 成员)
  • 将该值反馈回控件以进行显示

对于所有“单一事实来源”的说法,Apple 似乎是在撒谎——在这个链中注入一个验证阶段似乎很困难,尤其是在不破坏视图封装的情况下

请注意,我并不想特别解决 this 问题 - 我正在寻找一种模式来实现(即:将 StringTextField 替换为 Bool例如Toggle

以下代码显示了我在执行上述循环时的最佳尝试

class ValidatedValue<T>: ObservableObject {

    let objectWillChange = ObservableObjectPublisher()

    var validator: (T, T)->T

    var value: T {
        get {
            _value
        }
        set {
            _value = validator(_value, newValue)
            objectWillChange.send()
        }
    }

    /// Backing value for the observable
    var _value: T

    init(_ value: T, validator: @escaping (T, T)->T) {
        self._value = value
        self.validator = validator
    }
}

struct MustHaveDTextField: View {

    @ObservedObject var editingValue: ValidatedValue<String>

    public var body: some View {
        return TextField(
            "Must have a d",
            text: $editingValue.value
    }
}

View 范围之外定义的验证值

ValidatedValue(
    "oddity has a d",
    validator: { current, new in
        if new.contains("d") {
            return new
        }
        else {
            return current
        }
    }
)

这种 起作用,因为它会阻止您修改不包含“d”的字符串输入。然而;

  • 光标状态仍然在文本控件上移动,超过了验证点
  • 它暴露了应该完全是内部状态的内容,并需要从父母那里或通过EnvironmentObject 传递(如果你正在使用Lists 的东西......ow)

要么我遗漏了一些关键,要么我采用了错误的方法,要么 Apple 所说的不是 Apple 所做的。

herehere 那样在循环期间修改内部状态并不好——它们会修改视图循环内的状态,XCode 将其标记为undefined behaviourThis one 也有类似的解决方案,但同样需要将验证逻辑 放在视图之外 - 恕我直言,它应该是独立的。

【问题讨论】:

    标签: validation swiftui combine


    【解决方案1】:

    我不会声称这是唯一正确的方法,但我处理此问题的一种方法是将处理后的验证规则(在我的情况下是在组合管道中编码)的结果表示为结果属性:

    • validationMessages: String[]
    • isEverythingOK: Boolean

    我通过为用户可用的字段输入公开@Published 属性来连接验证,并在模型上将每个属性与组合主题配对,使用属性上的didSet{} 闭包发送更新。然后,验证规则全部包含在模型上的组合管道中,并且只公开结果。

    我在 Using Combine 中处理它的一些示例代码,可以在 Github 上的 ReactiveForm.swiftReactiveFormModel.swift 获得

    例如,我将尝试在此处包含相关位。请注意,在示例中,我有意为 SwiftUI 视图公开发布者,但实际上只是为了表明它是可能的 - 并不是说​​它是解决此特定解决方案的一种方法。

    在实践中,我发现当表单验证时,形式化或确切知道您想要显示的内容对我开发解决方案的方式产生了巨大影响。

    import Foundation
    import Combine
    
    class ReactiveFormModel : ObservableObject {
    
        @Published var firstEntry: String = "" {
            didSet {
                firstEntryPublisher.send(self.firstEntry)
            }
        }
        private let firstEntryPublisher = CurrentValueSubject<String, Never>("")
    
        @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)
                .map { (arg) -> [String] in
                    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
                .map { stringArray in
                    return stringArray.count < 1
                }
                .eraseToAnyPublisher()
    
            let _ = validationPipeline
                .assign(to: \.validationMessages, on: self)
                .store(in: &cancellableSet)
        }
    }
    

    【讨论】: