Validação de dados em aplicativos iOS

Acho que cada um de nós enfrentou a tarefa de validar dados em aplicativos. Por exemplo, ao registrar um usuário, você precisa verificar se o email está no formato correto, se a senha atende aos requisitos de segurança e assim por diante. Você pode dar muitos exemplos, mas tudo se resume a uma tarefa - validação de dados antes de enviar o formulário.


Enquanto trabalhava no próximo projeto, pensei em criar uma solução universal, em vez de escrever métodos de validação para cada tela com um formulário separadamente. O Swift 5.1 introduziu a anotação @propertyWrapper , e pensei que seria conveniente ter uma sintaxe como a seguinte:


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

Validadores


O primeiro passo foi determinar a aparência e o funcionamento dos validadores. Como parte do meu projeto, eu precisava verificar a validade dos dados e exibir as mensagens de erro apropriadas. Não havia necessidade de tratamento de erros mais complexo, portanto a implementação do validador foi a seguinte:


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

Tudo é simples aqui. O validador contém uma mensagem de erro que lança um ValidationError se a validação falhar. Isso simplifica o tratamento de erros, pois todos os validadores retornam o mesmo tipo de erro, mas com mensagens diferentes. Um exemplo é o código de um validador que verifica uma cadeia de caracteres com uma expressão regular:


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

Esta implementação contém um problema conhecido por muitos. Como o protocolo Validator contém um tipo associatedtype , não podemos criar uma variável do tipo


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

Para resolver esse problema, usamos uma abordagem padrão, a saber, a criação da estrutura 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) } } 

Eu acho que não há nada para comentar. Essa é uma abordagem padrão para resolver o problema descrito acima. Também seria útil adicionar uma extensão para o protocolo Validator, permitindo criar um objeto AnyValidator.

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

Wrapper de propriedade


Com os validadores resolvidos, você pode ir diretamente para a implementação do 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 } } 

O objetivo deste artigo não é analisar como propertyWrapper wrappers propertyWrapper funcionam e que sintaxe eles usam. Se você ainda não conseguiu encontrá-los, aconselho a ler meu outro artigo, Como abordar wrappers para propriedades Swift (inglês).


Essa implementação nos permite declarar propriedades que requerem validação da seguinte maneira:


 @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 

E obtenha uma matriz de erros de validação a qualquer momento, como a seguir


 let errors = $email.errors 

imagem
Existe a possibilidade de que algumas combinações de validadores (por exemplo, validação de email) apareçam no aplicativo em várias telas. Para evitar copiar o código, nesses casos, é possível criar um wrapper separado herdado 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 

Infelizmente, no momento, a anotação @propertyWrapper requer a substituição de wrappedValue e projectedValue , caso contrário, obteremos um erro de compilação. Parece um bug de implementação, portanto, é possível que em versões futuras do Swift isso seja corrigido.


Adicionar reativo


Juntamente com o iOS13, surgiu a estrutura de programação reativa nativa da Combine. E eu pensei que poderia ser útil ter a seguinte sintaxe:


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

Isso permitirá atualizar informações sobre erros de validação em tempo real (após cada caractere digitado). A implementação inicial dessa ideia foi a seguinte:


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

Devido ao fato de que a propriedade armazenada não pode ser marcada com a anotação @available , eu tive que usar a _subject com as propriedades _subject e subject . Caso contrário, tudo deve ficar muito claro. Um PassthroughObject é criado e envia mensagens sempre que o wrappedValue alterado.


Como resultado, as mensagens de erro de validação são alteradas à medida que o usuário preenche o formulário.
imagem
No processo de teste dessa solução, um bug foi revelado. A validação ocorre sempre que uma propriedade é alterada, independentemente da presença de assinantes deste evento. Por um lado, isso não afeta o resultado, mas, por outro lado, caso não precisemos de validação em tempo real, ações desnecessárias serão executadas. Ele validará e enviará mensagens corretamente somente se houver pelo menos um assinante. Como resultado, o código foi refeito com esse requisito.


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

Como resultado, obtive uma solução relativamente universal para validação de dados no aplicativo. Pode não resolver alguns problemas, por exemplo, manipulação de erros mais complicada do que a simples saída de mensagens, mas é adequado para validação simples da entrada do usuário. Você pode conferir a solução completa no GitHub .

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


All Articles