实现具有嵌入错误消息/用户反馈的可组合谓词类型



我有一个基于简单闭包的"Swifty"版本的NSPredcate。这使得它是可组合的,但我想找到一种实现错误消息的方法,在UI中向用户提供反馈。

当我试图用逻辑AND组合两个谓词时,问题就出现了——在我当前的实现(使谓词非常简单(中,我找不到从组件谓词生成错误消息的有意义的方法。一个明显的解决方案是向谓词添加一个计算属性,该属性将重新评估谓词并返回错误(如果适用(,但这似乎效率很低。

我开始研究通过Combine Publisher公开错误消息,但这很快就失控了,看起来不必要地复杂。我得出的结论是,我现在看不到木头,只能用一点牛。代码库如下。。。

谓词:

public struct Predicate<Target> {
// MARK: Public roperties
var matches: (Target) -> Bool
var error: String
// MARK: Init
init(_ matcher: @escaping (Target) -> Bool, error: String = "") {
self.matches = matcher
self.error = error
}
// MARK: Factory methods
static func required<LosslessStringComparabke: Collection>() -> Predicate<LosslessStringComparabke> {
.init( { !$0.isEmpty }, error: "Required field")
}
static func characterCountMoreThan<LosslessStringComparable: Collection>(count: Int) -> Predicate<LosslessStringComparable> {
.init({ $0.count >= count }, error: "Length must be at least (count) characters")
}
static func characterCountLessThan<LosslessStringComparable: Collection>(count: Int) -> Predicate<LosslessStringComparable> {
.init( { $0.count <= count }, error: "Length must be less than (count) characters")
}
static func characterCountWithin<LosslessStringComparable: Collection>(range: Range<Int>) -> Predicate<LosslessStringComparable> {
.init({ ($0.count >= range.lowerBound) && ($0.count <= range.upperBound) }, error: "Length must be between (range.lowerBound) and (range.upperBound) characters")
}
}

// MARK: Overloads
// e.g. let uncompletedItems = list.items(matching: .isCompleted == false)
func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate { $0[keyPath: lhs] == rhs }
}
// r.g. let uncompletedItems = list.items(matching: !.isCompleted)
prefix func !<T>(rhs: KeyPath<T, Bool>) -> Predicate<T> {
rhs == false
}

func ><T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate { $0[keyPath: lhs] > rhs }
}

func <<T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
//    Predicate { $0[keyPath: lhs] < rhs }
Predicate({ $0[keyPath: lhs] < rhs }, error: "(rhs) must be less than (lhs)")
}

func &&<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
return Predicate({ lhs.matches($0) && rhs.matches($0) }, error: "PLACEHOLDER: One predicate failed")
}
func ||<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
Predicate({ lhs.matches($0) || rhs.matches($0) }, error: "PLACEHOLDER: Both predicates failed")
}

验证器(使用谓词(:

public enum ValidationError: Error, CustomStringConvertible {
case generic(String)
public var description: String {
switch self {
case .generic(let error): return error
}
}
}
public struct Validator<ValueType> {
private var predicate: Predicate<ValueType>
func validate(_ value: ValueType) -> Result<ValueType, ValidationError> {
switch predicate.matches(value) {
case true:
return .success(value)
case false:
return .failure(.generic(predicate.error)) // TODO: placeholder
}
}
init(predicate: Predicate<ValueType>) {
self.predicate = predicate
}
}

Validator结构由属性包装器使用:

@propertyWrapper
public class ValidateAndPublishOnMain<ValueType> where ValueType: LosslessStringConvertible { // Type constraint specifically for SwiftUI text controls
@Published private var value: ValueType
private var validator: Validator<ValueType>
public var wrappedValue: ValueType {
get { value }
set { value = newValue }
}
// need to also force validation to execute when the textfield loses focus
public var projectedValue: AnyPublisher<Result<ValueType, ValidationError>, Never> {
return $value
.receive(on: DispatchQueue.main)
.map { value in
self.validator.validate(value)
}
.eraseToAnyPublisher()
}
public init(wrappedValue initialValue: ValueType, predicate: Predicate<ValueType>) {
self.value = initialValue
self.validator = Validator(predicate: predicate)
}
}

最后,在SwiftUI(和相关的视图模型(中使用属性包装器

public class ViewModel: ObservableObject {
@ValidateAndPublishOnMain(predicate: .required() && .characterCountLessThan(count: 5))
var validatedData = "" {
willSet { objectWillChange.send() }
}
var errorMessage: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
setupBindings()
}
private func setupBindings() {
$validatedData
.map { value in
switch value {
case .success: return ""
case .failure(let error): return error.description
}
}
.assign(to: .errorMessage, on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
@State private var error = ""
var body: some View {
VStack {
HStack {
Text("Label")
TextField("Data here", text: $viewModel.validatedData)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
Text("Result: (viewModel.validatedData)")
Text("Errors: (viewModel.errorMessage)")
}
.onAppear {
self.viewModel.objectWillChange.send() // ensures UI shows requirements immediately
}
}
}

出现歧义的主要原因是错误消息过早"定型"。对于&&操作,在对表达式求值之前,您不会知道错误消息。

因此,不应存储error属性。相反,只有当matches返回时才输出错误消息,即作为其返回值。当然,您还需要处理没有错误消息的成功状态。

Swift提供了许多建模方法——您可以返回代表错误消息的String?,或者Result<(), ValidationError>,甚至Result<Target, ValidationError>

只要您将错误消息设置为matches的返回值(无论您选择哪种类型(,就不应该存在这种模糊性问题。

在这里,我已经用Result<(), ValidationError>完成了。老实说,代码本身我很简单:

public struct ValidationError: Error {
let message: String
}
public struct Predicate<Target> {
var matches: (Target) -> Result<(), ValidationError>
// MARK: Factory methods
static func required<T: Collection>() -> Predicate<T> {
.init { !$0.isEmpty ? .success(()) : .failure(ValidationError(message: "Required field")) }
}
static func characterCountMoreThan<T: StringProtocol>(count: Int) -> Predicate<T> {
.init { $0.count > count ? .success(()) : .failure(ValidationError(message: "Length must be more than (count) characters")) }
}
static func characterCountLessThan<T: StringProtocol>(count: Int) -> Predicate<T> {
.init { $0.count < count ? .success(()) : .failure(ValidationError(message: "Length must be less than (count) characters")) }
}
static func characterCountWithin<T: StringProtocol>(range: Range<Int>) -> Predicate<T> {
.init {
($0.count >= range.lowerBound) && ($0.count <= range.upperBound) ?
.success(()) :
.failure(ValidationError(message: "Length must be between (range.lowerBound) and (range.upperBound) characters")) }
}
}
func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate {
$0[keyPath: lhs] == rhs ?
.success(()) :
.failure(ValidationError(message: "Must equal (rhs)"))
}
}
// r.g. let uncompletedItems = list.items(matching: !.isCompleted)
prefix func !<T>(rhs: KeyPath<T, Bool>) -> Predicate<T> {
rhs == false
}
func ><T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate {
$0[keyPath: lhs] > rhs ?
.success(()) :
.failure(ValidationError(message: "Must be greater than (rhs)"))
}
}
func <<T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate {
$0[keyPath: lhs] < rhs ?
.success(()) :
.failure(ValidationError(message: "Must be less than (rhs)"))
}
}

func ||<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
// short-circuiting version, needs a nested switch
//    Predicate {
//        target in
//        switch lhs.matches(target) {
//        case .success:
//            return .success(())
//        case .failure(let leftError):
//            switch rhs.matches(target) {
//            case .success:
//                return .success(())
//            case .failure(let rightError):
//                return .failure(ValidationError(message: "(leftError.message) AND (rightError.message)"))
//            }
//        }
//    }
// without a nested switch, not short-circuiting
Predicate {
target in
switch (lhs.matches(target), rhs.matches(target)) {
case (.success, .success), (.success, .failure), (.failure, .success):
return .success(())
case (.failure(let leftError), .failure(let rightError)):
return .failure(ValidationError(message: "(leftError.message) AND (rightError.message)"))
}
}
}
func &&<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
Predicate {
target in
switch (lhs.matches(target), rhs.matches(target)) {
case (.success, .success):
return .success(())
case (.success, let rightFail):
return rightFail
case (let leftFail, .success):
return leftFail
case (.failure(let leftError), .failure(let rightError)):
return .failure(ValidationError(message: "(leftError.message) AND (rightError.message)"))
}
}
}
@propertyWrapper
public class ValidateAndPublishOnMain<ValueType> where ValueType: LosslessStringConvertible { // Type constraint specifically for SwiftUI text controls
@Published private var value: ValueType
private var validator: Predicate<ValueType>
public var wrappedValue: ValueType {
get { value }
set { value = newValue }
}
// need to also force validation to execute when the textfield loses focus
public var projectedValue: AnyPublisher<Result<ValueType, ValidationError>, Never> {
return $value
.receive(on: DispatchQueue.main)
.map { value in
// mapped the Result' Success type
self.validator.matches(value).map { _ in value }
}
.eraseToAnyPublisher()
}
public init(wrappedValue initialValue: ValueType, predicate: Predicate<ValueType>) {
self.value = initialValue
self.validator = predicate
}
}

请注意,我已将ValidationError更改为结构,而不是枚举。如果你不喜欢ValidationError(message: ...)的冗长,你可以让它符合ExpressibleByStringLiteral

您可能需要考虑的另一件事是涉及关键路径的谓词的消息。密钥路径没有可读的字符串表示,因此不能将"isCompleted must equals false"作为.isCompleted == false的消息。

最新更新