Como abordar wrappers para propriedades Swift
Publicados: 2022-03-11Em termos simples, um wrapper de propriedade é uma estrutura genérica que encapsula o acesso de leitura e gravação à propriedade e adiciona um comportamento adicional a ela. Nós o usamos se precisarmos restringir os valores de propriedade disponíveis, adicionar lógica extra ao acesso de leitura/gravação (como usar bancos de dados ou padrões do usuário) ou adicionar alguns métodos adicionais.
Este artigo é sobre uma nova abordagem do Swift 5.1 para encapsular propriedades, que apresenta uma sintaxe nova e mais limpa.
Abordagem Antiga
Imagine que você está desenvolvendo um aplicativo e tem um objeto que contém dados de perfil do usuário.
struct Account { var firstName: String var lastName: String var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email = "[email protected]" print(account.email)
Você deseja adicionar verificação de e-mail — se o endereço de e-mail do usuário não for válido, a propriedade de e- email
deverá ser nil
. Este seria um bom caso para usar um wrapper de propriedade para encapsular essa lógica.
struct Email<Value: StringProtocol> { private var _value: Value? init(initialValue value: Value?) { _value = value } var value: Value? { get { return validate(email: _value) ? _value : nil } set { _value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}" let pred = NSPredicate(format: "SELF MATCHES %@", regex) return pred.evaluate(with: email) } }
Podemos usar este wrapper na estrutura da conta:
struct Account { var firstName: String var lastName: String var email: Email<String> }
Agora, temos certeza de que a propriedade email pode conter apenas um endereço de email válido.
Tudo parece bom, exceto a sintaxe.
let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)
Com um wrapper de propriedade, a sintaxe para inicializar, ler e gravar tais propriedades se torna mais complexa. Então, é possível evitar essa complicação e usar wrappers de propriedade sem alterações de sintaxe? Com o Swift 5.1, a resposta é sim.
A Nova Maneira: @propertyWrapper Anotação
O Swift 5.1 fornece uma solução mais elegante para criar wrappers de propriedade, onde é permitido marcar um wrapper de propriedade com uma anotação @propertyWrapper
. Esses wrappers possuem sintaxe mais compacta em comparação com os tradicionais, resultando em um código mais compacto e compreensível. A anotação @propertyWrapper
tem apenas um requisito: seu objeto wrapper deve conter uma propriedade não estática chamada wrappedValue
.
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
Para definir essa propriedade encapsulada no código, precisamos usar a nova sintaxe.
@Email var email: String?
Então, marcamos a propriedade com a anotação @
email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil
Ótimo, parece melhor agora do que com a abordagem antiga. Mas nossa implementação de wrapper tem uma desvantagem: ela não permite um valor inicial para o valor encapsulado.
@Email var email: String? = "[email protected]" //compilation error.
Para resolver isso, precisamos adicionar o seguinte inicializador ao wrapper:
init(wrappedValue value: Value?) { self.value = value }
E é isso.
@Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil
O código final do wrapper está abaixo:
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
Wrappers configuráveis
Vamos dar outro exemplo. Você está escrevendo um jogo e tem uma propriedade na qual as pontuações do usuário são armazenadas. O requisito é que esse valor seja maior ou igual a 0 e menor ou igual a 100. Você pode conseguir isso usando um wrapper de propriedade.
@propertyWrapper struct Scores { private let minValue = 0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } } @Scores var scores: Int = 0
Este código funciona, mas não parece genérico. Você não pode reutilizá-lo com restrições diferentes (não 0 e 100). Além disso, ele pode restringir apenas valores inteiros. Seria melhor ter um wrapper configurável que pudesse restringir qualquer tipo que esteja em conformidade com o protocolo Comparable. Para tornar nosso wrapper configurável, precisamos adicionar todos os parâmetros de configuração por meio de um inicializador. Se o inicializador contém um atributo wrappedValue
(o valor inicial de nossa propriedade), ele deve ser o primeiro parâmetro.

@propertyWrapper struct Constrained<Value: Comparable> { private var range: ClosedRange<Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange<Value>) { self.value = value self.range = range } var wrappedValue: Value { get { return max(min(value, range.upperBound), range.lowerBound) } set { value = newValue } } }
Para inicializar uma propriedade encapsulada, definimos todos os atributos de configuração entre parênteses após a anotação.
@Constrained(0...100) var scores: Int = 0
O número de atributos de configuração é ilimitado. Você precisa defini-los entre parênteses na mesma ordem do inicializador.
Obtendo acesso ao próprio wrapper
Se você precisar acessar o wrapper em si (não o valor encapsulado), precisará adicionar um sublinhado antes do nome da propriedade. Por exemplo, vamos pegar nossa estrutura de contas.
struct Account { var firstName: String var lastName: String @Email var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email // Wrapped value (String) account._email // Wrapper(Email<String>)
Precisamos de acesso ao próprio wrapper para usar a funcionalidade adicional que adicionamos a ele. Por exemplo, queremos que a estrutura da conta esteja em conformidade com o protocolo Equatable. Duas contas são iguais se seus endereços de e-mail forem iguais e os endereços de e-mail não devem diferenciar maiúsculas de minúsculas.
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }
Funciona, mas não é a melhor solução, pois devemos lembrar de adicionar um método em minúsculas() sempre que compararmos e-mails. Uma maneira melhor seria tornar a estrutura do Email igualável:
extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }
e compare wrappers em vez de valores encapsulados:
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }
Valor projetado
A anotação @propertyWrapper
fornece mais um açúcar de sintaxe - um valor projetado. Esta propriedade pode ter qualquer tipo que desejar. Para acessar esta propriedade, você precisa adicionar um prefixo $
ao nome da propriedade. Para explicar como funciona, usamos um exemplo da estrutura Combine.
O wrapper de propriedade @Published
cria um publicador para a propriedade e o retorna como um valor projetado.
@Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher
Como você pode ver, usamos uma mensagem para acessar a propriedade encapsulada e uma $message para acessar o editor. O que você deve fazer para adicionar um valor projetado ao seu wrapper? Nada de especial, apenas declare.
@propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }
Conforme observado anteriormente, a propriedade projectedValue
pode ter qualquer tipo com base em suas necessidades.
Limitações
A sintaxe dos novos wrappers de propriedade parece boa, mas também contém várias limitações, sendo as principais:
- Eles não podem participar do tratamento de erros. O valor encapsulado é uma propriedade (não um método) e não podemos marcar o getter ou o setter como
throws
. Por exemplo, em nosso exemplo de e-Email
, não é possível gerar um erro se um usuário tentar definir um e-mail inválido. Podemos retornarnil
ou travar o aplicativo com uma chamadafatalError()
, o que pode ser inaceitável em alguns casos. - A aplicação de vários wrappers à propriedade não é permitida. Por exemplo, seria melhor ter um wrapper
@CaseInsensitive
separado e combiná-lo com um wrapper@Email
em vez de tornar o wrapper@Email
insensível a maiúsculas e minúsculas. Mas construções como essas são proibidas e levam a erros de compilação.
@CaseInsensitive @Email var email: String?
Como solução para esse caso específico, podemos herdar o wrapper Email
do wrapper CaseInsensitive
. No entanto, a herança também tem limitações - somente classes suportam herança e apenas uma classe base é permitida.
Conclusão
As anotações @propertyWrapper
simplificam a sintaxe dos wrappers de propriedade e podemos operar com as propriedades encapsuladas da mesma maneira que com as comuns. Isso torna seu código, como desenvolvedor Swift, mais compacto e compreensível. Ao mesmo tempo, tem várias limitações que temos que levar em conta. Espero que alguns deles sejam corrigidos em futuras versões do Swift.
Se você quiser saber mais sobre as propriedades do Swift, confira os documentos oficiais.