SwiftUI 验证和否决用户输入
Posted
技术标签:
【中文标题】SwiftUI 验证和否决用户输入【英文标题】:SwiftUI validation and vetoing for user input 【发布时间】:2019-10-01 05:57:58 【问题描述】:我希望在 SwiftUI 中实现一个通用的验证/否决循环 - 使用“单一事实来源”框架应该非常简单的事情
总之我想:
拥有一个通用控件(比如TextField
)
对该控件的更新应用验证/否决(例如,用户键入文本)
将预期的更改传播到验证器,在某处更新 Binding
源对象(理想情况下,View
内的 @State
成员)
将该值反馈回控件以进行显示
对于所有“单一事实来源”的说法,Apple 似乎是在撒谎——在这个链中注入一个验证阶段似乎很困难,尤其是在不破坏视图封装的情况下
请注意,我并不想特别解决 this 问题 - 我正在寻找一种模式来实现(即:将 String
和 TextField
替换为 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
传递(如果你正在使用List
s 的东西......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 - 如何在 TextField 上设置最大数量?