Validation des données dans les applications iOS

Je pense que chacun de nous a été confronté à la tâche de valider les données dans les applications. Par exemple, lors de l'enregistrement d'un utilisateur, vous devez vous assurer que le courrier électronique a le format correct et que le mot de passe répond aux exigences de sécurité, etc. Vous pouvez donner beaucoup d'exemples, mais tout se résume à une seule tâche - la validation des données avant de soumettre le formulaire.


En travaillant sur le prochain projet, j'ai pensé à créer une solution universelle, plutôt que d'écrire des méthodes de validation pour chaque écran avec un formulaire séparément. Swift 5.1 a introduit l'annotation @propertyWrapper , et j'ai pensé qu'il serait pratique d'avoir une syntaxe comme celle-ci:


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

Validateurs


La première étape consistait à déterminer l'apparence et le fonctionnement des validateurs. Dans le cadre de mon projet, je devais vérifier la validité des données et afficher les messages d'erreur appropriés. Il n'était pas nécessaire de gérer des erreurs plus complexes, la mise en œuvre du validateur était donc la suivante:


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

Ici, tout est simple. Le validateur contient un message d'erreur qui déclenche une ValidationError si la validation échoue. Cela simplifie la gestion des erreurs, car tous les validateurs renvoient le même type d'erreur, mais avec des messages différents. Un exemple est le code d'un validateur qui vérifie une chaîne par rapport à une expression régulière:


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

Cette implémentation contient un problème connu de nombreux. Étant donné que le protocole Validator contient un type associatedtype , nous ne pouvons pas créer une variable de type


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

Pour résoudre ce problème, nous utilisons une approche standard, à savoir la création de la structure 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) } } 

Je pense qu'il n'y a rien à commenter. Il s'agit d'une approche standard pour résoudre le problème décrit ci-dessus. Il serait également utile d'ajouter une extension pour le protocole Validator, vous permettant de créer un objet AnyValidator.

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

Habillage de propriété


Une fois les valideurs triés, vous pouvez passer directement à l'implémentation du wrapper @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 } } 

Le but de cet article n'est pas d'analyser le fonctionnement propertyWrapper wrappers propertyWrapper et la syntaxe qu'ils utilisent. Si vous n'avez pas encore pu les rencontrer, je vous conseille de lire mon autre article, How to Approach Wrappers for Swift Properties (anglais).


Cette implémentation nous permet de déclarer les propriétés qui nécessitent une validation comme suit:


 @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 

Et obtenez un tableau d'erreurs de validation à tout moment comme suit


 let errors = $email.errors 

image
Il est possible que certaines combinaisons de validateurs (par exemple, la validation par e-mail) apparaissent dans l'application sur plusieurs écrans. Afin d'éviter de copier le code, dans de tels cas, il est possible de créer un wrapper distinct hérité de 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 

Malheureusement, pour le moment, l'annotation @propertyWrapper nécessite de wrappedValue et projectedValue , sinon nous obtiendrons une erreur de compilation. Il ressemble à un bogue d'implémentation, il est donc possible que dans les futures versions de Swift cela soit corrigé.


Ajouter réactif


Avec iOS13, le cadre de programmation réactive Combine natif est apparu. Et j'ai pensé qu'il pourrait être utile d'avoir la syntaxe suivante:


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

Cela permettra de mettre à jour les informations sur les erreurs de validation en temps réel (après chaque caractère entré). La mise en œuvre initiale de cette idée était la suivante:


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

Étant donné que la propriété stockée ne peut pas être marquée avec l'annotation @available , j'ai dû utiliser une _subject contournement avec les propriétés _subject et subject . Sinon, tout devrait être très clair. Un PassthroughObject est créé qui envoie des messages chaque fois que le wrappedValue change.


Par conséquent, les messages d'erreur de validation changent lorsque l'utilisateur remplit le formulaire.
image
Lors du test de cette solution, un bogue a été révélé. La validation a lieu chaque fois qu'une propriété change, quelle que soit la présence des abonnés à cet événement. D'une part, cela n'affecte pas le résultat, mais d'autre part, dans le cas où nous n'avons pas besoin de validation en temps réel, des actions inutiles seront effectuées. Il validera et enverra correctement les messages uniquement s'il y a au moins un abonné. En conséquence, le code a été refait avec cette exigence.


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

En conséquence, j'ai obtenu une solution relativement universelle pour la validation des données dans l'application. Il peut ne pas résoudre certains problèmes, par exemple, une gestion des erreurs plus compliquée que la simple sortie de message, mais il convient à une validation simple de l'entrée utilisateur. Vous pouvez consulter la solution complète sur GitHub .

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


All Articles