أعتقد أن كل واحد منا واجه مهمة التحقق من صحة البيانات في التطبيقات. على سبيل المثال ، عند تسجيل أحد المستخدمين ، تحتاج إلى التأكد من أن البريد الإلكتروني له التنسيق الصحيح ، وأن كلمة المرور تلبي متطلبات الأمان ، وما إلى ذلك. يمكنك إعطاء الكثير من الأمثلة ، لكن كل ذلك يتلخص في مهمة واحدة - التحقق من صحة البيانات قبل إرسال النموذج.
أثناء العمل في المشروع التالي ، فكرت في إنشاء حل عالمي ، بدلاً من كتابة طرق التحقق من الصحة لكل شاشة بنموذج منفصل. قدم Swift 5.1 @propertyWrapper
توضيحيًا @propertyWrapper
، واعتقدت أنه سيكون من المناسب أن يكون لديك بناء جملة مثل ما يلي:
@Validated([validator1, validator2, ...]) var email: String? = nil let errors = $email.errors
المصادقون
كانت الخطوة الأولى هي تحديد كيف سيبدو المدققون ويعملون. كجزء من مشروعي ، كنت بحاجة للتحقق من صحة البيانات وعرض رسائل الخطأ المناسبة. ليست هناك حاجة لمعالجة الأخطاء بشكل أكثر تعقيدًا ، لذلك كان تنفيذ المدقق كما يلي:
struct ValidationError: LocalizedError { var message: String public var errorDescription: String? { message } } protocol Validator { associatedtype ValueType var errorMessage: String { get } func isValid(value: ValueType?) -> Bool } extension Validator { func validate(value: ValueType?) throws { if !isValid(value: value) { throw ValidationError(message: errorMessage) } } }
كل شيء بسيط هنا. يحتوي المدقق على رسالة خطأ ترمي ValidationError في حالة فشل التحقق من الصحة. يعمل ذلك على تبسيط معالجة الأخطاء ، نظرًا لأن جميع أجهزة التحقق من الصحة تقوم بإرجاع نفس نوع الخطأ ، ولكن مع رسائل مختلفة. مثال على ذلك هو رمز المدقق الذي يتحقق من سلسلة مقابل تعبير عادي:
struct RegexValidator: Validator { public var errorMessage: String private var regex: String public init(regex: String, errorMessage: String) { self.regex = regex self.errorMessage = errorMessage } public func isValid(value: String?) -> Bool { guard let v = value else { return false } let predicate = NSPredicate(format: "SELF MATCHES %@", regex) return predicate.evaluate(with: v) } }
يحتوي هذا التطبيق على مشكلة معروفة للعديد من المشاكل. نظرًا لأن بروتوكول Validator
يحتوي على نوع associatedtype
، لا يمكننا إنشاء متغير نوع
var validators:[Validator]
لحل هذه المشكلة ، نستخدم مقاربة قياسية ، وهي إنشاء بنية AnyValidator.
private class ValidatorBox<T>: Validator { var errorMessage: String { fatalError() } func isValid(value: T?) -> Bool { fatalError() } } private class ValidatorBoxHelper<T, V:Validator>: ValidatorBox<T> where V.ValueType == T { private let validator: V init(validator: V) { self.validator = validator } override var errorMessage: String { validator.errorMessage } override func isValid(value: T?) -> Bool { validator.isValid(value: value) } } struct AnyValidator<T>: Validator { private let validator: ValidatorBox<T> public init<V: Validator>(validator: V) where V.ValueType == T { self.validator = ValidatorBoxHelper(validator: validator) } public var errorMessage: String { validator.errorMessage } public func isValid(value: T?) -> Bool { validator.isValid(value: value) } }
أعتقد أنه لا يوجد شيء للتعليق عليه. هذا هو نهج قياسي لحل المشكلة المذكورة أعلاه. قد يكون من المفيد أيضًا إضافة ملحق لبروتوكول Validator ، مما يسمح لك بإنشاء كائن AnyValidator.
extension Validator { var validator: AnyValidator<ValueType> { AnyValidator(validator: self) } }
المجمع الملكية
مع فرز @Validated
من @Validated
، يمكنك الانتقال مباشرةً إلى تطبيق برنامج @Validated
من @Validated
.
@propertyWrapper class Validated<Value> { private var validators: [AnyValidator<Value>] var wrappedValue: Value? init(wrappedValue value: Value?, _ validators: [AnyValidator<Value>]) { wrappedValue = value self.validators = validators } var projectedValue: Validated<Value> { self } public var errors: [ValidationError] { var errors: [ValidationError] = [] validators.forEach { do { try $0.validate(value: wrappedValue) } catch { errors.append(error as! ValidationError) } } return errors } }
ليس الغرض من هذه المقالة هو تحليل كيفية عمل مغلفات propertyWrapper
وماهي بناء الجملة الذي يستخدمونه. إذا لم تتمكن من مقابلتها حتى الآن ، فإنني أنصحك بقراءة مقالتي الأخرى ، كيفية التعامل مع أغلفة خصائص سويفت (الإنجليزية).
يسمح لنا هذا التطبيق بإعلان الخصائص التي تتطلب التحقق من الصحة على النحو التالي:
@Validated([ NotEmptyValidator(errorMessage: "Email can't be empty").validator, RegexValidator(regex:"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64} ", errorMessage:"Email has wrong format").validator ]) var email: String? = nil
والحصول على مجموعة من أخطاء التحقق في أي وقت على النحو التالي
let errors = $email.errors

هناك احتمال أن تظهر بعض مجموعات أدوات التحقق (على سبيل المثال ، التحقق من صحة البريد الإلكتروني) في التطبيق على شاشات متعددة. لتجنب نسخ الشفرة ، في مثل هذه الحالات ، من الممكن إنشاء غلاف منفصل موروث من Validated
.
@propertyWrapper final class Email: Validated<String> { override var wrappedValue: String? { get { super.wrappedValue } set { super.wrappedValue = newValue } } override var projectedValue: Validated<String> { super.projectedValue } init(wrappedValue value: String?) { let notEmptyValidator = NotEmptyValidator(errorMessage: "Email can't be empty") let regexValidator = RegexValidator(regex:"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64} ", errorMessage:"Email has wrong format").validator super.init(wrappedValue: value, [notEmptyValidator, regexValidator]) } } @Email var email: String? = nil
لسوء الحظ ، في الوقت الحالي ، يتطلب التعليق التوضيحي @propertyWrapper
تجاوز wrappedValue
و projectedValue
، وإلا فسوف نحصل على خطأ في wrappedValue
. يبدو وكأنه خطأ في التنفيذ ، لذلك فمن الممكن أن يتم إصلاح ذلك في الإصدارات المستقبلية من Swift.
أضف رد الفعل
جنبا إلى جنب مع iOS13 ، جاء إطار الجمع بين البرمجة التفاعلية على طول. وأعتقد أنه قد يكون من المفيد الحصول على بناء الجملة التالي:
let cancellable = $email .publisher .map { $0.map { $0.localizedDescription }.joined(separator: ", ") } .receive(on: RunLoop.main) .assign(to: \.text, on: emailErrorLabel)
سيسمح هذا بتحديث المعلومات حول أخطاء التحقق من الصحة في الوقت الفعلي (بعد إدخال كل حرف). كان التنفيذ الأولي لهذه الفكرة على النحو التالي:
@propertyWrapper class Validated<Value> { private var _subject: Any! @available(iOS 13.0, *) private var subject: PassthroughSubject<[ValidationError], Never> { return _subject as! PassthroughSubject<[ValidationError], Never> } open var wrappedValue: Value? { didSet { if #available(iOS 13.0, *) { subject.send(errors) } } } public init(wrappedValue value: Value?, _ validators: [AnyValidator<Value>]) { wrappedValue = value self.validators = validators if #available(iOS 13.0, *) { _subject = PassthroughSubject<[ValidationError], Never>() } } @available(iOS 13.0, *) public var publisher: AnyPublisher<[ValidationError], Never> { subject.eraseToAnyPublisher() }
نظرًا لحقيقة أنه لا يمكن وضع علامة على الخاصية المخزنة مع التعليق التوضيحي @available
، اضطررت لاستخدام حل مع خصائص subject
_subject
. خلاف ذلك ، يجب أن يكون كل شيء واضح جدا. يتم إنشاء PassthroughObject
يرسل رسائل في كل مرة يتغير فيها wrappedValue
.
نتيجة لذلك ، تتغير رسائل خطأ التحقق من الصحة أثناء قيام المستخدم بتعبئة النموذج.

في عملية اختبار هذا الحل ، تم الكشف عن خطأ واحد. يحدث التحقق من الصحة في كل مرة تتغير فيها خاصية ، بغض النظر عن وجود المشتركين في هذا الحدث. من ناحية ، لا يؤثر هذا على النتيجة ، ولكن من ناحية أخرى ، في حالة عدم حاجتنا إلى التحقق في الوقت الفعلي ، سيتم تنفيذ إجراءات غير ضرورية. سيتم التحقق من صحة وإرسال الرسائل بشكل صحيح فقط إذا كان هناك مشترك واحد على الأقل. نتيجة لذلك ، تمت إعادة صياغة الرمز مع هذا المطلب.
@propertyWrapper class Validated<Value> { private var _subject: Any! @available(iOS 13.0, *) private var subject: Publishers.HandleEvents<PassthroughSubject<[ValidationError], Never>> { return _subject as! Publishers.HandleEvents<PassthroughSubject<[ValidationError], Never>> } private var subscribed: Bool = false open var wrappedValue: Value? { didSet { if #available(iOS 13.0, *) { if subscribed { subject.upstream.send(errors) } } } } public init(wrappedValue value: Value?, _ validators: [AnyValidator<Value>]) { wrappedValue = value self.validators = validators if #available(iOS 13.0, *) { _subject = PassthroughSubject<[ValidationError], Never>() .handleEvents(receiveSubscription: {[weak self] _ in self?.subscribed = true }) } }
نتيجة لذلك ، حصلت على حل عالمي نسبياً للتحقق من صحة البيانات في التطبيق. قد لا يحل بعض المشكلات ، على سبيل المثال ، معالجة الأخطاء بشكل أكثر تعقيدًا من إخراج الرسائل البسيطة ، لكنه مناسب للتحقق من صحة إدخال المستخدم. يمكنك التحقق من الحل الكامل على جيثب .