Creo que cada uno de nosotros se ha enfrentado a la tarea de validar datos en aplicaciones. Por ejemplo, al registrar un usuario, debe asegurarse de que el correo electrónico tenga el formato correcto y que la contraseña cumpla con los requisitos de seguridad, etc. Puede dar muchos ejemplos, pero todo se reduce a una tarea: la validación de datos antes de enviar el formulario.
Mientras trabajaba en el próximo proyecto, pensé en crear una solución universal, en lugar de escribir métodos de validación para cada pantalla con un formulario por separado. Swift 5.1 introdujo la anotación @propertyWrapper
, y pensé que sería conveniente tener una sintaxis como la siguiente:
@Validated([validator1, validator2, ...]) var email: String? = nil let errors = $email.errors
Validadores
El primer paso fue determinar cómo se verían y funcionarían los validadores. Como parte de mi proyecto, necesitaba verificar la validez de los datos y mostrar los mensajes de error apropiados. No hubo necesidad de un manejo de errores más complejo, por lo que la implementación del validador fue la siguiente:
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) } } }
Todo es simple aquí. El validador contiene un mensaje de error que arroja un ValidationError si la validación falla. Esto simplifica el manejo de errores, ya que todos los validadores devuelven el mismo tipo de error, pero con diferentes mensajes. Un ejemplo es el código de un validador que verifica una cadena contra una expresión 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 implementación contiene un problema conocido por muchos. Como el protocolo Validator
contiene un tipo associatedtype
, no podemos crear una variable de tipo
var validators:[Validator]
Para resolver este problema, utilizamos un enfoque estándar, a saber, la creación de la estructura 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) } }
Creo que no hay nada que comentar. Este es un enfoque estándar para resolver el problema descrito anteriormente. También sería útil agregar una extensión para el protocolo Validator, permitiéndole crear un objeto AnyValidator.
extension Validator { var validator: AnyValidator<ValueType> { AnyValidator(validator: self) } }
Envoltorio de propiedad
Con los validadores ordenados, puede ir directamente a la implementación del contenedor @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 } }
El propósito de este artículo no es analizar cómo funcionan propertyWrapper
wrappers propertyWrapper
y qué sintaxis usan. Si aún no ha podido conocerlos, le aconsejo que lea mi otro artículo Cómo acercarse a los contenedores para propiedades Swift (inglés).
Esta implementación nos permite declarar propiedades que requieren validación de la siguiente manera:
@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
Y obtenga una serie de errores de validación en cualquier momento dado de la siguiente manera
let errors = $email.errors

Existe la posibilidad de que algunas combinaciones de validadores (por ejemplo, validación de correo electrónico) aparezcan en la aplicación en varias pantallas. Para evitar copiar el código, en tales casos es posible crear un contenedor separado heredado 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
Desafortunadamente, en este momento, la anotación @propertyWrapper
requiere anular wrappedValue
y projectedValue
, de lo contrario obtendremos un error de compilación. Parece un error de implementación, por lo que es posible que en futuras versiones de Swift esto se solucione.
Añadir reactivo
Junto con iOS13, apareció el marco de programación reactivo nativo Combine. Y pensé que podría ser útil tener la siguiente sintaxis:
let cancellable = $email .publisher .map { $0.map { $0.localizedDescription }.joined(separator: ", ") } .receive(on: RunLoop.main) .assign(to: \.text, on: emailErrorLabel)
Esto permitirá actualizar información sobre errores de validación en tiempo real (después de cada carácter ingresado). La implementación inicial de esta idea fue la siguiente:
@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() }
Debido al hecho de que la propiedad almacenada no se puede marcar con la anotación @available
, tuve que usar el trabajo con las propiedades _subject
y subject
. De lo contrario, todo debería estar muy claro. Se crea un PassthroughObject
que envía mensajes cada vez que cambia el valor de wrappedValue
.
Como resultado, los mensajes de error de validación cambian a medida que el usuario completa el formulario.

En el proceso de probar esta solución, se reveló un error. La validación se produce cada vez que cambia una propiedad, independientemente de la presencia de suscriptores a este evento. Por un lado, esto no afecta el resultado, pero por otro lado, en el caso de que no necesitemos validación en tiempo real, se realizarán acciones innecesarias. Validará y enviará mensajes correctamente solo si hay al menos un suscriptor. Como resultado, el código se rehizo con este 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, obtuve una solución relativamente universal para la validación de datos en la aplicación. Es posible que no resuelva algunos problemas, por ejemplo, un manejo de errores más complicado que la simple salida de mensajes, pero es adecuado para la validación simple de la entrada del usuario. Puede consultar la solución completa en GitHub .