SwiftUI 验证和否决用户输入

Posted

技术标签:

【中文标题】SwiftUI 验证和否决用户输入【英文标题】:SwiftUI validation and vetoing for user input 【发布时间】:2019-10-01 05:57:58 【问题描述】:

我希望在 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 所做的。

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

【问题讨论】:

【参考方案1】:

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

validationMessages: String[] isEverythingOK: Boolean

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

我在 Using Combine 中处理它的一些示例代码,可以在 Github 上的 ReactiveForm.swift 和 ReactiveFormModel.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)
    

【讨论】:

以上是关于SwiftUI 验证和否决用户输入的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI 中将我的环境对象设置为用户输入?

SwiftUI 和 Core Data:基于用户输入获取

SwiftUI - 如何在 TextField 上设置最大数量?

如何让用户在 SwiftUI 文本字段中仅使用数字输入货币,同时保留 $ 和 .?

从 SwiftUI 列表中的每个元素获取用户输入

当用户输入的时间低于预期时间时,如何在 swiftUI 中显示警报消息?