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]
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

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

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