Introduits dans Swift 5.1, les wrappers de propriétés sont rapidement devenus l'une des fonctionnalités les plus utilisées dans tout ce qui concerne Swift. Le célèbre framework SwiftUI n'aurait probablement pas pu être ce qu'il est aujourd'hui sans ses @State
, @Binding
, @StateObject
et autres éléments de magie noire que vous utilisez tous les jours. Ou peut-être avez-vous entendu parler ou même utilisé l'un des frameworks de mappage relationnel d'objets (ORM) les plus populaires pour un serveur côté Swift Vapor appelé Fluent . Ils ne vous laissent pas d'autre choix que de déterminer comment et quand utiliser un signe dollar $
avant une propriété soigneusement enveloppée dans un autre wrapper.
La vérité est que les wrappers de propriétés ne sont pas si difficiles à déchiffrer lorsque vous plongez profondément et essayez de les utiliser par vous-même. En fait, vous avez probablement un tas d'endroits dans votre projet où vous pouvez faciliter les choses en apportant un wrapper pour vous aider. Dans ce guide, je couvrirai 5 cas d'utilisation de base comme celui-ci. Mais d'abord, rafraîchissons-nous la mémoire sur le sujet lui-même.
Un wrapper de propriété est un type spécialement annoté qui encapsule un morceau de logique appliqué à toutes les propriétés en général ou aux propriétés limitées à un type spécifique que vous choisissez. Il peut s'agir d'une structure, d'une énumération ou d'une classe déclarée avec un attribut @propertyWrapper
et possédant au moins une propriété wrappedValue
obligatoire à l'intérieur :
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { // mandatory property get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } }
À l'intérieur d'un getter et d'un setter de la propriété de valeur encapsulée, vous pouvez ajouter toute la logique dont vous avez besoin et intercepter, transformer ou utiliser les valeurs de la manière que vous souhaitez. Vous pouvez ensuite utiliser le wrapper comme ceci :
class SomeService { @SomeWrapper fancyNumber: Int init(number: Int) { self._number = SomeWrapper(wrappedValue: number) // use "_" to initialize a property wrapper } func doSmth() { print(fancyNumber) } }
L'avantage des wrappers de propriétés est que vous pouvez ajouter vos propres initialiseurs, méthodes supplémentaires, propriétés, etc. qui peuvent avoir toutes les fonctionnalités dont vous avez besoin. L'une d'entre elles est une propriété projectedValue
facultative qui peut renvoyer même un wrapper lui-même ou tout autre type. Maintenant, rappelez-vous : lorsque vous souhaitez accéder à quelque chose qui appartient au wrapper lui-même, mais pas à sa valeur, vous devez utiliser un signe dollar notoire :
@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 } }
Bien sûr, tous les exemples absurdes ci-dessus ne démontrent pas suffisamment la véritable puissance des wrappers de propriétés, alors examinons quelques-unes des manières les plus utiles de les faire briller.
Inspirés par tout ce que Fluent peut faire, nous pouvons appliquer des wrappers de propriétés pour gérer UserDefaults
au lieu de créer un service séparé pour cela de cette façon :
@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) } } }
Comme vous pouvez le voir, le wrapper ci-dessus peut être utilisé avec tout ce qui Codable
@DefaultsStorage(key: "upload_date", defaultValue: Date()) var uploadDate: Date
L'implémentation ci-dessus peut être modifiée et modifiée de nombreuses fois en fonction de vos besoins. Par exemple, vous pouvez la rendre vraiment polyvalente en acceptant d'autres types ou en autorisant l'utilisation UserDefaults
basés sur une suite au lieu du singleton standard
prêt à l'emploi.
Une autre chose est les données que vous enregistrez avec. Si vous souhaitez stocker quelque chose de manière sécurisée, vous souhaiterez peut-être écrire une solution similaire pour le trousseau.
Au fait, Apple dispose déjà d'un wrapper similaire appelé @AppStorage
, mais il a été conçu pour être utilisé avec SwiftUI, ce que vous ne souhaitez peut-être pas nécessairement.
Dans les cas où vous devez surveiller une valeur spécifique, le wrapper de propriété suivant peut vous être utile :
@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)
Ce wrapper de propriété pourrait devenir encore plus utile avec l'injection d'un service d'analyse supplémentaire. Il peut s'agir d'un élément local ou même distant. Si vous utilisez ce dernier, n'oubliez pas de filtrer au niveau du site d'appel de ce service pour éviter les appels inutiles.
Nous devons souvent valider les entrées des utilisateurs. Il existe de nombreux types d'entrées possibles, comme l'e-mail, un numéro de téléphone, une date de naissance, etc. Tous ces éléments nécessitent une logique spécifique appliquée aux données d'origine. Vous pouvez résoudre ce problème en utilisant un wrapper de propriétés pour chaque entrée :
@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 vous pouvez même essayer une solution universelle comme celle-ci :
@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: ... } } }
Similairement à la validation des éléments, vous pouvez formater des dates avec des wrappers de propriétés en définissant un ensemble de formats préfixés que vous utiliseriez à l'intérieur du wrapper.
Parfois, vous devez limiter une valeur dans une certaine plage, par exemple en définissant un pourcentage compris entre 0 et 100 ou une valeur fractionnaire comprise entre 0 et 1, etc. Vous pouvez également encapsuler cette logique dans un wrapper de propriété :
@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) } } }
Vous pouvez l'utiliser comme ceci :
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 autre cas d'utilisation des wrappers de propriétés est de nous aider avec tout ce qui Codable
. L'implémentation d'une logique d'encodage et de décodage personnalisée peut être placée en toute sécurité dans un wrapper si nécessaire. Vous pouvez également encoder automatiquement une chaîne en base64 avec cette solution simple :
@propertyWrapper struct Base64Encoding { private var value = "" var wrappedValue: String { get { Data(value.utf8).base64EncodedString() } set { value = newValue } } }
En utilisant le wrapper ci-dessus, chaque fois que vous accédez au texte transformé par celui-ci, il renverra toujours une chaîne codée en base64.
Cependant, lorsque vous utilisez des wrappers de propriétés, n'oubliez pas qu'il s'agit des mêmes types Swift de base que nous connaissons depuis des années, mais avec des fonctionnalités supplémentaires ajoutées. L'une des nombreuses idées populaires sur la façon d'utiliser un wrapper de propriétés dans le passé était d'essayer de sécuriser les propriétés avec celui-ci. Vous pouvez trouver plusieurs implémentations de @Atomic
en ligne, mais il s'est avéré que la plupart d'entre elles sont assez problématiques . Même si des correctifs peuvent également être trouvés, je ne recommande plus d'utiliser @Atomic
, en particulier avec les acteurs désormais disponibles pour servir cet objectif.
J'espère que vous avez beaucoup appris ou au moins rafraîchi vos connaissances sur les wrappers de propriétés dans Swift. Avec ce rappel, jetez un œil à vos projets actuels : vous avez peut-être des éléments qui peuvent exploiter la puissance de cette fonctionnalité !