التحقق من صحة البيانات في تطبيقات iOS

أعتقد أن كل واحد منا واجه مهمة التحقق من صحة البيانات في التطبيقات. على سبيل المثال ، عند تسجيل أحد المستخدمين ، تحتاج إلى التأكد من أن البريد الإلكتروني له التنسيق الصحيح ، وأن كلمة المرور تلبي متطلبات الأمان ، وما إلى ذلك. يمكنك إعطاء الكثير من الأمثلة ، لكن كل ذلك يتلخص في مهمة واحدة - التحقق من صحة البيانات قبل إرسال النموذج.


أثناء العمل في المشروع التالي ، فكرت في إنشاء حل عالمي ، بدلاً من كتابة طرق التحقق من الصحة لكل شاشة بنموذج منفصل. قدم 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] //Protocol 'Validator' can only be used as a generic constraint because it has Self or associated type requirements 

لحل هذه المشكلة ، نستخدم مقاربة قياسية ، وهي إنشاء بنية 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() } // The rest of the code } 

نظرًا لحقيقة أنه لا يمكن وضع علامة على الخاصية المخزنة مع التعليق التوضيحي @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 }) } } // The rest of the code } 

نتيجة لذلك ، حصلت على حل عالمي نسبياً للتحقق من صحة البيانات في التطبيق. قد لا يحل بعض المشكلات ، على سبيل المثال ، معالجة الأخطاء بشكل أكثر تعقيدًا من إخراج الرسائل البسيطة ، لكنه مناسب للتحقق من صحة إدخال المستخدم. يمكنك التحقق من الحل الكامل على جيثب .

Source: https://habr.com/ru/post/ar485092/


All Articles