Introduzidos no Swift 5.1, os wrappers de propriedade rapidamente se tornaram um dos recursos mais usados em todas as coisas relacionadas ao Swift. O famoso framework SwiftUI provavelmente não seria do jeito que é agora sem seus @State
, @Binding
, @StateObject
e outras peças de magia negra que você usa todos os dias. Ou talvez você tenha ouvido ou até mesmo usado um dos frameworks de mapeadores relacionais de objetos (ORM) mais populares para um Vapor do Swift do lado do servidor chamado Fluent . Eles não deixam você com escolha para descobrir como e quando usar um cifrão $
antes de uma propriedade perfeitamente envolvida em outro wrapper.
A verdade é que os wrappers de propriedade não são tão difíceis de quebrar quando você se aprofunda e tenta usá-los por conta própria. Na verdade, você provavelmente tem vários lugares no seu projeto onde pode facilitar as coisas trazendo um wrapper para ajudar. Neste guia, cobrirei 5 casos básicos de uso como esse. Mas, primeiro, vamos refrescar nossas memórias sobre o assunto em si.
Um wrapper de propriedade é um tipo especialmente anotado que encapsula um pedaço de lógica aplicada a quaisquer propriedades em geral ou propriedades restritas a algum tipo específico que você escolher. Pode ser uma struct, enum ou classe que declarou com um atributo @propertyWrapper
e tem pelo menos um wrappedValue
como uma propriedade obrigatória dentro:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { // mandatory property get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } }
Dentro de um getter e um setter da propriedade de valor encapsulado, você pode adicionar qualquer lógica que precisar e interceptar, transformar ou usar valores basicamente de qualquer maneira que desejar. Então você pode usar o wrapper assim:
class SomeService { @SomeWrapper fancyNumber: Int init(number: Int) { self._number = SomeWrapper(wrappedValue: number) // use "_" to initialize a property wrapper } func doSmth() { print(fancyNumber) } }
O que é legal sobre wrappers de propriedade é que você pode adicionar seus próprios inicializadores, métodos adicionais, propriedades, etc. que podem ter qualquer funcionalidade que você precise. Uma delas é uma propriedade projectedValue
opcional que pode retornar até mesmo um wrapper em si ou qualquer outro tipo. Agora, lembre-se: quando você quer acessar algo que pertence ao wrapper em si, mas não ao seu valor, você precisa usar um cifrão notório:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } func methodToDoSmth() { ... } // extra wrapper method } class SomeService { ... func doSmth() { $fancyNumber.methodToDoSmth() // here we access the logic of the wrapper itself } }
É claro que todos os exemplos absurdos acima não demonstram o suficiente o verdadeiro poder dos wrappers de propriedade, então vamos dar uma olhada em algumas das maneiras mais úteis de fazê-los brilhar.
Inspirados por tudo o que o Fluent pode fazer, podemos aplicar wrappers de propriedade para lidar com UserDefaults
em vez de criar um serviço separado para isso desta maneira:
@propertyWrapper struct DefaultsStorage<T: Codable> { private let key: String private let defaultValue: T init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { // getting and decodind the data from UserDefaults guard let data = UserDefaults.standard.object(forKey: key) as? Data, let value = try? JSONDecoder().decode(T.self, from: data) else { return defaultValue } return value } set { // encodind the data and saving to UserDefaults guard let encoded = try? JSONEncoder().encode(newValue) else { return } let defaults = UserDefaults.standard defaults.set(encoded, forKey: key) } } }
Como você pode ver, o wrapper acima pode ser usado com qualquer coisa Codable
@DefaultsStorage(key: "upload_date", defaultValue: Date()) var uploadDate: Date
A implementação acima pode ser ajustada e alterada bastante, dependendo das suas necessidades. Por exemplo, você pode torná-la verdadeiramente versátil aceitando outros tipos ou permitir o uso UserDefaults
baseados em suíte em vez do singleton standard
pronto para uso.
Outra coisa são os dados que você salva com ele. Se você quer armazenar algo com segurança, você pode querer escrever uma solução similar para o Keychain.
A propósito, a Apple já tem um wrapper semelhante chamado @AppStorage
, mas ele foi projetado para ser usado com o SwiftUI, o que você pode não necessariamente querer.
Nos casos em que você precisa monitorar um valor específico, o seguinte wrapper de propriedade pode ser útil:
@propertyWrapper struct Logged<Value> { private var value: Value private let label: String // label that will be used in the log event init(wrappedValue: Value, label: String) { self.value = wrappedValue self.label = label print("\(label): Initial value set to \(value)") } var wrappedValue: Value { get { value } set { // prints the set event to a console print("\(label): Value changed from \(value) to \(newValue)") value = newValue } } }
@Logged(label: "Total Price") var price = Double(12.5)
Este wrapper de propriedade pode se tornar ainda mais útil com serviço de análise adicional injetado. Pode ser algo local ou até mesmo remoto. Se estiver usando o último, lembre-se de filtrar no site de chamada de tal serviço para evitar chamadas desnecessárias.
Muitas vezes precisamos validar a entrada do usuário. Há muitos tipos de entrada possíveis, como e-mail, número de telefone, data de nascimento e assim por diante. Todos eles exigem uma parte lógica específica aplicada aos dados originais. Você pode resolver isso usando um wrapper de propriedade para cada entrada:
@propertyWrapper struct ValidatedEmail<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } init(wrappedValue value: Value?) { self.value = value } private func validate(email: Value?) -> Bool { guard let 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) } }
Ou você pode até tentar uma solução única para todos como esta:
@propertyWrapper struct Validated<Value> { // define all the possible inputs enum InputType { case email case phoneNumber ... } let inputType: InputType var value: Value? var wrappedValue: Value? { get { // return value if validation succeeds or nil if not return validate(value) ? value : nil } set { value = newValue } } init(inputType: InputType, wrappedValue value: Value?) { self.inputType = inputType self.value = value } private func validate(_ value: Value?) -> Bool { // include validation logic for every input type switch inputType { case .email: ... case .phoneNumber: ... } } }
Semelhante à validação de coisas, você pode formatar datas com wrappers de propriedade definindo um conjunto de formatos prefixados que você usaria dentro do wrapper.
Às vezes, você precisa limitar um valor em um certo intervalo, por exemplo, tendo uma porcentagem abrangendo de 0 a 100 ou um valor de fração de 0 a 1 e assim por diante. Você pode encapsular essa lógica em um wrapper de propriedade também:
@propertyWrapper struct Clamped<Value: Comparable> { private var value: Value private let range: ClosedRange<Value> init(wrappedValue: Value, _ range: ClosedRange<Value>) { self.range = range self.value = min(max(wrappedValue, range.lowerBound), range.upperBound) } var wrappedValue: Value { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } }
Você pode usar assim:
struct ClampedExample { @Clamped(0...10) var number: Int init(number: Int) { self._number = Clamped(wrappedValue: number, 0...10) } func clampTest() { number = 20 print(example.number) // 10, because 20 is clamped to the upper bound 10 number = -5 print(example.number) // 0, because -5 is clamped to the lower bound 0 } }
Outro caso de uso para wrappers de propriedade é nos ajudar com todas as coisas Codable
. A implementação de lógica de codificação e decodificação personalizada pode ser colocada com segurança dentro de um wrapper, se necessário. Você também pode codificar automaticamente uma String em base64 com esta solução simples:
@propertyWrapper struct Base64Encoding { private var value = "" var wrappedValue: String { get { Data(value.utf8).base64EncodedString() } set { value = newValue } } }
Usando o wrapper acima, sempre que você acessar o texto transformado por ele, ele sempre retornará uma string codificada em base64.
No entanto, ao lidar com wrappers de propriedade, não se esqueça de que eles são os mesmos tipos básicos do Swift que conhecemos há anos, mas com um recurso extra adicionado. Uma das muitas ideias populares sobre como usar um wrapper de propriedade no passado era tentar propriedades thread-safe com ele. Você pode encontrar várias implementações de @Atomic
online, mas descobriu-se que a maioria delas é bastante problemática . Embora também possam ser encontradas correções para ele, não recomendo mais usar @Atomic
, especialmente com atores agora disponíveis para atender a esse propósito.
Espero que você tenha aprendido bastante ou pelo menos tenha refrescado seu conhecimento sobre wrappers de propriedade em Swift. Com esta atualização, dê uma olhada em seus projetos atuais: talvez você tenha algumas peças que podem alavancar o poder deste recurso!