Swift 使用嵌入的错误消息/用户反馈实现可组合谓词类型
我有一个基于简单闭包的NSPredicate的“快速”版本。这使得它可以组合,但我想找到一种实现错误消息的方法,以便在UI中向用户提供反馈 当我试图用逻辑AND组合两个谓词时,问题就出现了——在我当前的实现中(它使谓词非常简单),我找不到从组件谓词生成错误消息的有意义的方法。一个明显的解决方案是向谓词添加一个computed属性,该属性将重新计算谓词并返回一个错误(如果适用),但这似乎效率很低 我开始研究通过联合发布器公开错误消息,但这很快就失去了控制,看起来不必要的复杂。我的结论是,我现在看不到树木的树木,可以做一点引导。代码库如下 谓词:Swift 使用嵌入的错误消息/用户反馈实现可组合谓词类型,swift,swiftui,predicate,Swift,Swiftui,Predicate,我有一个基于简单闭包的NSPredicate的“快速”版本。这使得它可以组合,但我想找到一种实现错误消息的方法,以便在UI中向用户提供反馈 当我试图用逻辑AND组合两个谓词时,问题就出现了——在我当前的实现中(它使谓词非常简单),我找不到从组件谓词生成错误消息的有意义的方法。一个明显的解决方案是向谓词添加一个computed属性,该属性将重新计算谓词并返回一个错误(如果适用),但这似乎效率很低 我开始研究通过联合发布器公开错误消息,但这很快就失去了控制,看起来不必要的复杂。我的结论是,我现在看不
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")
}
公共结构谓词{
//马克:公共财产
变量匹配:(目标)->Bool
变量错误:字符串
//MARK:Init
init(matcher:@escaping(Target)->Bool,错误:String=”“){
self.matches=匹配器
self.error=错误
}
//标记:工厂方法
static func required()->谓词{
.init({!$0.isEmpty},错误:“必填字段”)
}
静态func characterCountMoreThan(count:Int)->谓词{
.init({$0.count>=count},错误:“长度必须至少为\(count)个字符”)
}
静态func characterCountLessThan(count:Int)->谓词{
.init({$0.count谓词){
.init({($0.count>=range.lowerBound)&($0.count谓词){
谓词{$0[keyPath:lhs]==rhs}
}
//r.g.let uncompletedItems=列表项(匹配:!\.isCompleted)
前缀func!(rhs:KeyPath)->谓词{
rhs==假
}
func>(lhs:KeyPath,rhs:V)->谓词{
谓词{$0[keyPath:lhs]>rhs}
}
func谓词{
//谓词{$0[keyPath:lhs]谓词{
返回谓词({lhs.matches($0)&&rhs.matches($0)},错误:“占位符:一个谓词失败”)
}
func | |(lhs:谓词,rhs:谓词)->谓词{
谓词({lhs.matches($0)| | rhs.matches($0)},错误:“占位符:两个谓词都失败”)
}
验证器(使用谓词):
公共枚举验证错误:错误,CustomStringConvertible{
大小写通用(字符串)
公共变量说明:字符串{
切换自身{
case.generic(let error):返回错误
}
}
}
公共结构验证器{
私有变量谓词:谓词
func validate(value:ValueType)->结果{
开关谓词.matches(值){
大小写正确:
返回。成功(值)
案例错误:
return.failure(.generic(predicate.error))//TODO:占位符
}
}
init(谓词:谓词){
self.predicate=谓词
}
}
验证程序结构由属性包装器使用:
@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)
}
}
@propertyWrapper
public类ValidateAndPublishOnMain其中ValueType:LosslessStringConvertible{//Type约束专门用于SwiftUI文本控件
@已发布的私有var值:ValueType
私有var验证器:验证器
公共var wrappedValue:ValueType{
获取{value}
设置{value=newValue}
}
//当文本字段失去焦点时,还需要强制执行验证
public var projectedValue:AnyPublisher{
返回$value
.receive(在:DispatchQueue.main上)
.map{中的值
self.validator.validate(值)
}
.删除任何发布者()
}
public init(wrappedValue初始值:ValueType,谓词:谓词){
self.value=初始值
self.validator=验证器(谓词:谓词)
}
}
…最后,在SwiftUI中使用属性包装器(以及相关的视图模型)
public类ViewModel:observeObject{
@ValidateAndPublishOnMain(谓词:.required()&&.characterCountLessThan(计数:5))
var validatedData=“”{
willSet{objectWillChange.send()}
}
var errorMessage:String=“”
private var cancelables=Set()
init(){
setupBindings()
}
专用函数设置绑定(){
$validatedData
.map{中的值
开关量{
案例.成功:返回“”
case.failure(let error):返回error.description
}
}
.assign(发送至:\.errorMessage,打开:self)
.store(在:&可取消项中)
}
}
结构ContentView:View{
@ObservedObject var viewModel=viewModel()
@状态私有变量错误=“”
var body:一些观点{
VStack{
HStack{
文本(“标签”)
TextField(“此处的数据”,text:$viewModel.validatedData)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
文本(“结果:\(viewModel.validatedData)”)
文本(“错误:\(viewModel.errorMessage)”)
}
奥纳佩尔先生{
self.viewModel.objectWillChange.send()//确保UI立即显示需求
}
}
}
产生歧义的主要原因是错误消息“一成不变”太早了。对于&&
操作,在计算表达式之前,您不知道错误消息
因此,您不应该存储error
属性。相反,只在匹配时输出错误消息,即作为其返回值。当然,您还需要处理没有错误消息的成功状态
Swift提供了多种建模方法-您可以返回字符串
@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)
}
}
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
}
}
}
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
}
}