Introdotti in Swift 5.1, i wrapper di proprietà sono rapidamente diventati una delle funzionalità più utilizzate in tutto ciò che riguarda Swift. Tutto il famoso framework SwiftUI ora non avrebbe potuto probabilmente essere quello che è ora senza i suoi @State
, @Binding
, @StateObject
e altri pezzi di magia nera che usi ogni giorno. O forse hai sentito o addirittura utilizzato uno dei più popolari framework di object relational mapper (ORM) per un Vapor di Swift lato server chiamato Fluent . Non ti lasciano altra scelta per capire come e quando utilizzare un simbolo di dollaro $
prima di una proprietà ordinatamente racchiusa in un altro wrapper.
La verità è che i wrapper di proprietà non sono così difficili da decifrare quando ci si immerge a fondo e si cerca di usarli da soli. In effetti, probabilmente ci sono un sacco di posti nel progetto in cui è possibile semplificare le cose portando un wrapper per aiutare. In questa guida tratterò 5 casi d'uso di base come questo. Ma prima, rinfreschiamo la memoria sull'argomento stesso.
Un wrapper di proprietà è un tipo annotato in modo speciale che incapsula un pezzo di logica applicato a qualsiasi proprietà in generale o proprietà vincolate a un tipo specifico scelto da te. Può essere una struct, un enum o una classe dichiarata con un attributo @propertyWrapper
e che ha almeno un wrappedValue
come proprietà obbligatoria all'interno:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { // mandatory property get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } }
All'interno di un getter e di un setter della proprietà valore wrapper puoi aggiungere qualsiasi logica ti serva e intercettare, trasformare o usare i valori in qualsiasi modo tu voglia. Quindi puoi usare il wrapper in questo modo:
class SomeService { @SomeWrapper fancyNumber: Int init(number: Int) { self._number = SomeWrapper(wrappedValue: number) // use "_" to initialize a property wrapper } func doSmth() { print(fancyNumber) } }
Ciò che è bello dei wrapper di proprietà è che puoi aggiungere i tuoi inizializzatori, metodi aggiuntivi, proprietà, ecc. che possono avere qualsiasi funzionalità di cui hai bisogno. Una di queste è una proprietà opzionale projectedValue
che può restituire anche un wrapper stesso o qualsiasi altro tipo. Ora, ricorda: quando vuoi accedere a qualcosa che appartiene al wrapper stesso, ma non al suo valore, devi usare un famigerato simbolo del dollaro:
@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 } }
Naturalmente, tutti gli esempi insensati sopra riportati non dimostrano a sufficienza la vera potenza dei property wrapper, quindi diamo un'occhiata ad alcuni dei modi più utili per farli risaltare.
Ispirati da tutte le cose che Fluent può fare, possiamo applicare wrapper di proprietà per gestire UserDefaults
invece di creare un servizio separato per questo in questo modo:
@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) } } }
Come puoi vedere, il wrapper sopra può essere utilizzato con qualsiasi cosa Codable
@DefaultsStorage(key: "upload_date", defaultValue: Date()) var uploadDate: Date
L'implementazione di cui sopra può essere modificata e cambiata molto a seconda delle tue esigenze. Ad esempio, puoi renderla davvero versatile accettando altri tipi o consentendo di usare UserDefaults
basati su suite invece del singleton standard
preimpostato.
Un'altra cosa sono i dati che salvi con esso. Se vuoi archiviare qualcosa in modo sicuro, potresti voler scrivere una soluzione simile per Keychain.
A proposito, Apple ha già un wrapper simile chiamato @AppStorage
, ma è stato progettato per essere utilizzato con SwiftUI, cosa che potresti non volere.
Nei casi in cui è necessario monitorare un valore specifico, può essere utile il seguente wrapper di proprietà:
@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)
Questo wrapper di proprietà potrebbe diventare ancora più utile con l'iniezione di un servizio di analisi aggiuntivo. Può essere qualcosa di locale o persino remoto. Se si utilizza quest'ultimo, ricordarsi di filtrare nel sito di chiamata di tale servizio per evitare chiamate non necessarie.
Spesso dobbiamo convalidare l'input dell'utente. Ci sono così tanti tipi di input possibili, come e-mail, numero di telefono, data di nascita e così via. Tutti questi richiedono un pezzo logico specifico applicato ai dati originali. Puoi risolvere questo problema utilizzando un wrapper di proprietà per ogni input:
@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) } }
Oppure puoi anche provare una soluzione valida per tutti come questa:
@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: ... } } }
Similmente alla convalida delle cose, è possibile formattare le date con wrapper di proprietà definendo un set di formati con prefisso da utilizzare all'interno del wrapper.
A volte è necessario limitare un valore in un certo intervallo, ad esempio con una percentuale che va da 0 a 100 o un valore di frazione da 0 a 1 e così via. È possibile incapsulare questa logica anche in un wrapper di proprietà:
@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) } } }
Puoi utilizzarlo in questo modo:
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 } }
Un altro caso d'uso per i wrapper di proprietà è di aiutarci con tutto ciò che riguarda Codable
. L'implementazione della logica di codifica e decodifica personalizzata può essere tranquillamente inserita in un wrapper se necessario. Puoi anche codificare automaticamente una String in base64 con questa semplice soluzione:
@propertyWrapper struct Base64Encoding { private var value = "" var wrappedValue: String { get { Data(value.utf8).base64EncodedString() } set { value = newValue } } }
Utilizzando il wrapper di cui sopra, ogni volta che si accede al testo da esso trasformato, verrà sempre restituita una stringa codificata in base64.
Tuttavia, quando si ha a che fare con i wrapper di proprietà, non dimenticare che sono gli stessi tipi base di Swift che conosciamo da anni, ma con funzionalità extra aggiunte in cima. Una delle tante idee popolari su come usare un wrapper di proprietà in passato era provare a usare le proprietà thread-safe con esso. Puoi trovare diverse implementazioni di @Atomic
online, ma si è scoperto che la maggior parte di esse sono piuttosto problematiche . Anche se si possono trovare anche delle soluzioni, non consiglio più di usare @Atomic
, specialmente con gli attori ora disponibili per questo scopo.
Spero che tu abbia imparato molto o almeno rinfrescato la tua conoscenza dei wrapper di proprietà in Swift. Con questo ripasso dai un'occhiata ai tuoi progetti attuali: forse hai qualche pezzo che può sfruttare la potenza di questa funzionalità!