Validierung von Daten in iOS-Anwendungen

Ich denke, jeder von uns hat sich der Aufgabe gestellt, Daten in Anwendungen zu validieren. Wenn Sie beispielsweise einen Benutzer registrieren, müssen Sie sicherstellen, dass die E-Mail das richtige Format hat und das Kennwort den Sicherheitsanforderungen entspricht. Sie können viele Beispiele nennen, aber alles läuft auf eine Aufgabe hinaus - die Datenüberprüfung vor dem Absenden des Formulars.


Bei der Arbeit am nächsten Projekt dachte ich darüber nach, eine universelle Lösung zu erstellen, anstatt Validierungsmethoden für jeden Bildschirm mit einem Formular separat zu schreiben. Swift 5.1 führte die Annotation @propertyWrapper , und ich dachte, es wäre bequem, eine Syntax wie die folgende zu haben:


 @Validated([validator1, validator2, ...]) var email: String? = nil let errors = $email.errors //   

Validatoren


Der erste Schritt bestand darin zu bestimmen, wie die Validatoren aussehen und funktionieren würden. Im Rahmen meines Projekts musste ich die Gültigkeit der Daten überprüfen und die entsprechenden Fehlermeldungen anzeigen. Es war keine komplexere Fehlerbehandlung erforderlich, sodass der Validator wie folgt implementiert wurde:


 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) } } } 

Hier ist alles einfach. Der Validator enthält eine Fehlermeldung, die einen ValidationError auslöst, wenn die Validierung fehlschlägt. Dies vereinfacht die Fehlerbehandlung, da alle Prüfer den gleichen Fehlertyp mit unterschiedlichen Meldungen zurückgeben. Ein Beispiel ist der Code eines Validators, der einen String gegen einen regulären Ausdruck prüft:


 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) } } 

Diese Implementierung enthält ein bekanntes Problem. Da das Validator Protokoll einen associatedtype Typ enthält, können wir keine Variable vom Typ erstellen


 var validators:[Validator] //Protocol 'Validator' can only be used as a generic constraint because it has Self or associated type requirements 

Um dieses Problem zu lösen, verwenden wir einen Standardansatz, nämlich die Erstellung der AnyValidator-Struktur.

 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) } } 

Ich denke, es gibt nichts zu kommentieren. Dies ist ein Standardansatz zur Lösung des oben beschriebenen Problems. Es wäre auch nützlich, eine Erweiterung für das Validator-Protokoll hinzuzufügen, mit der Sie ein AnyValidator-Objekt erstellen können.

 extension Validator { var validator: AnyValidator<ValueType> { AnyValidator(validator: self) } } 

Eigenschaften-Wrapper


@Validated die Validatoren sortiert sind, können Sie direkt zur Implementierung des @Validated Wrappers @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 } } 

In diesem Artikel wird nicht analysiert, wie propertyWrapper Wrapper funktionieren und welche Syntax sie verwenden. Wenn Sie sie noch nicht kennengelernt haben, empfehle ich Ihnen, meinen anderen Artikel zu lesen: Vorgehensweise bei Wrappern für schnelle Eigenschaften (Englisch).


Diese Implementierung ermöglicht es uns, Eigenschaften zu deklarieren, die wie folgt validiert werden müssen:


 @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 

Und erhalten Sie eine Reihe von Überprüfungsfehlern zu einem bestimmten Zeitpunkt wie folgt


 let errors = $email.errors 

Bild
Es besteht die Möglichkeit, dass einige Kombinationen von Validatoren (z. B. E-Mail-Validierung) in der Anwendung auf mehreren Bildschirmen angezeigt werden. In solchen Fällen kann ein separater Wrapper erstellt werden, der von Validated geerbt wird, um das Kopieren des Codes zu vermeiden.


 @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 

Leider erfordert die Annotation wrappedValue derzeit das @propertyWrapper von wrappedValue und projectedValue , da sonst ein Kompilierungsfehler wrappedValue . Es sieht aus wie ein Implementierungsfehler, daher ist es möglich, dass dies in zukünftigen Versionen von Swift behoben wird.


Reaktiv hinzufügen


Zusammen mit iOS13 kam das native reaktive Programmier-Framework Combine auf den Markt. Und ich dachte, es könnte nützlich sein, die folgende Syntax zu haben:


 let cancellable = $email .publisher .map { $0.map { $0.localizedDescription }.joined(separator: ", ") } .receive(on: RunLoop.main) .assign(to: \.text, on: emailErrorLabel) 

Auf diese Weise können Informationen zu Validierungsfehlern in Echtzeit (nach jedem eingegebenen Zeichen) aktualisiert werden. Die anfängliche Umsetzung dieser Idee war wie folgt:


 @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 } 

Aufgrund der Tatsache, dass die gespeicherte Eigenschaft nicht mit der Anmerkung @available gekennzeichnet werden kann, musste ich mit den Eigenschaften _subject und subject _subject . Ansonsten sollte alles sehr klar sein. Es wird ein PassthroughObject erstellt, das bei wrappedValue Änderung von wrappedValue Nachrichten sendet.


Infolgedessen ändern sich Überprüfungsfehlermeldungen, wenn der Benutzer das Formular ausfüllt.
Bild
Beim Testen dieser Lösung wurde ein Fehler entdeckt. Die Überprüfung erfolgt bei jeder Änderung einer Eigenschaft, unabhängig von der Anwesenheit von Abonnenten dieses Ereignisses. Einerseits hat dies keinen Einfluss auf das Ergebnis, andererseits werden unnötige Aktionen ausgeführt, wenn keine Validierung in Echtzeit erforderlich ist. Es wird nur dann Nachrichten korrekt validieren und senden, wenn mindestens ein Teilnehmer vorhanden ist. Infolgedessen wurde der Code mit dieser Anforderung überarbeitet.


 @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 } 

Als Ergebnis erhielt ich eine relativ universelle Lösung für die Datenvalidierung in der Anwendung. Einige Probleme, z. B. eine kompliziertere Fehlerbehandlung als die einfache Nachrichtenausgabe, werden möglicherweise nicht gelöst. Sie eignen sich jedoch für die einfache Validierung von Benutzereingaben. Sie können die vollständige Lösung auf GitHub überprüfen.

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


All Articles