Si vous avez utilisé SwiftUI, vous avez probablement fait attention aux mots clés tels que @ObservedObject, @EnvironmentObject, @FetchRequest et ainsi de suite. Les wrappers de propriété (ci-après dénommés «wrappers de propriété») est une nouvelle fonctionnalité de Swift 5.1. Cet article vous aidera à comprendre d'où viennent toutes les constructions de @, comment les utiliser dans SwiftUI et dans vos projets.

Traduit par: Evgeny Zavozhansky, développeur de FunCorp.
Remarque: Au moment où la traduction a été préparée, une partie du code source de l'article d'origine avait perdu sa pertinence en raison de changements dans la langue, donc certains exemples de code ont été intentionnellement remplacés.
Les wrappers de propriétés ont été introduits pour la première fois sur les forums Swift en mars 2019, quelques mois avant l'annonce de SwiftUI. Dans sa proposition originale, Douglas Gregor, membre de l'équipe Swift Core, décrivait cette construction (alors appelée délégués de propriété) comme «une généralisation accessible aux utilisateurs des fonctionnalités actuellement fournies par une construction de langage telle que lazy
, par exemple».
Si une propriété est déclarée avec le mot-clé lazy
, cela signifie qu'elle sera initialisée lors du premier accès. Par exemple, l'initialisation différée d'une propriété peut être implémentée à l'aide d'une propriété privée, accessible via une propriété calculée. Mais l'utilisation du mot-clé lazy
rend cela beaucoup plus facile.
struct Structure {
SE-0258: Property Wrapper explique parfaitement la conception et la mise en œuvre des wrappers de propriété. Par conséquent, au lieu d'essayer d'améliorer la description dans la documentation officielle, considérez quelques exemples qui peuvent être implémentés à l'aide de wrappers de propriétés:
- restriction de la valeur des propriétés;
- conversion de valeurs lors du changement de propriétés;
- changer la sémantique de l'égalité et comparer les propriétés;
- enregistrement de l'accès à la propriété.
Limiter les valeurs de propriété
SE-0258: Property Wrapper fournit plusieurs exemples pratiques, notamment @Clamping
, @Copying
, @Atomic
, @ThreadSpecific
, @Box
, @UserDefault
. Considérez le wrapper @Clamping
, qui vous permet de limiter la valeur maximale ou minimale d'une propriété.
@propertyWrapper struct Clamping<Value: Comparable> { var value: Value let range: ClosedRange<Value> init(initialValue value: Value, _ range: ClosedRange<Value>) { precondition(range.contains(value)) self.value = value self.range = range } var wrappedValue: Value { get { value } set { value = min(max(range.lowerBound, newValue), range.upperBound) } } }
@Clamping
peut être utilisé, par exemple, pour modéliser l'acidité d'une solution, dont la valeur peut prendre une valeur de 0 à 14.
struct Solution { @Clamping(0...14) var pH: Double = 7.0 } let carbonicAcid = Solution(pH: 4.68)
Si vous tentez de définir la valeur du pH au-delà de la plage de (0...14)
, la propriété prendra la valeur la plus proche de l'intervalle minimum ou maximum.
let superDuperAcid = Solution(pH: -1) superDuperAcid.pH
Les wrappers de propriété peuvent être utilisés pour implémenter d'autres wrappers de propriété. Par exemple, le wrapper @UnitInterval
limite la valeur d'une propriété à l'intervalle (0...1)
aide de @Clamping(0...1)
:
@propertyWrapper struct UnitInterval<Value: FloatingPoint> { @Clamping(0...1) var wrappedValue: Value = .zero init(initialValue value: Value) { self.wrappedValue = value } }
Idées similaires
@Positive
/ @NonNegative
indique que la valeur peut être un nombre positif ou négatif.@NonZero
indique que la valeur de la propriété ne peut pas être 0.@Validated
ou @Whitelisted
/ @Blacklisted
restreint la valeur d'une propriété à certaines valeurs.
Conversion de valeurs lors de la modification des propriétés
La validation des valeurs des champs de texte est un casse-tête constant pour les développeurs d'applications. Il y a tellement de choses à garder à l'esprit: des platitudes telles que l'encodage aux tentatives malveillantes d'entrer un code via un champ de texte. Envisagez d'utiliser un wrapper de propriété pour supprimer les espaces qu'un utilisateur a saisis au début et à la fin d'une ligne.
import Foundation let url = URL(string: " https:
Foundation
propose la méthode trimmingCharacters(in:)
, avec laquelle vous pouvez supprimer des espaces au début et à la fin d'une ligne. Vous pouvez appeler cette méthode chaque fois que vous avez besoin de garantir l'exactitude de l'entrée, mais ce n'est pas très pratique. Vous pouvez utiliser le wrapper de propriété pour cela.
import Foundation @propertyWrapper struct Trimmed { private(set) var value: String = "" var wrappedValue: String { get { return value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } }
struct Post { @Trimmed var title: String @Trimmed var body: String } let quine = Post(title: " Swift Property Wrappers ", body: "…") quine.title
Idées similaires
@Transformed
applique la conversion ICU à la chaîne d' @Transformed
.@Rounded
/ @Truncated
arrondit ou tronque une valeur de chaîne.
Changer la sémantique de l'égalité et la comparaison des propriétés
Dans Swift, deux chaînes sont égales si elles sont canoniquement équivalentes , c'est-à-dire contiennent les mêmes caractères. Mais supposons que nous voulons que les propriétés de chaîne soient égales, et non sensibles à la casse, qu'elles contiennent.
@CaseInsensitive
implémente un wrapper pour les propriétés de type String
ou SubString
.
import Foundation @propertyWrapper struct CaseInsensitive<Value: StringProtocol> { var wrappedValue: Value } extension CaseInsensitive: Comparable { private func compare(_ other: CaseInsensitive) -> ComparisonResult { wrappedValue.caseInsensitiveCompare(other.wrappedValue) } static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedSame } static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedAscending } static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedDescending } }
let hello: String = "hello" let HELLO: String = "HELLO" hello == HELLO
Idées similaires
@Approximate
pour une comparaison grossière des propriétés de type Double ou Float.@Ranked
pour les propriétés dont les valeurs sont en ordre (par exemple, le rang des cartes à jouer).
Journalisation de l'accès aux propriétés
@Versioned
vous permettra d'intercepter les valeurs attribuées et de vous rappeler quand elles ont été définies.
import Foundation @propertyWrapper struct Versioned<Value> { private var value: Value private(set) var timestampedValues: [(Date, Value)] = [] var wrappedValue: Value { get { value } set { defer { timestampedValues.append((Date(), value)) } value = newValue } } init(initialValue value: Value) { self.wrappedValue = value } }
La classe ExpenseReport
permet d'enregistrer les horodatages des états de traitement du rapport de dépenses.
class ExpenseReport { enum State { case submitted, received, approved, denied } @Versioned var state: State = .submitted }
Mais l'exemple ci-dessus montre une sérieuse limitation dans l'implémentation actuelle des wrappers de propriétés, qui découle de la restriction Swift: les propriétés ne peuvent pas lever d'exceptions. Si nous voulions ajouter une restriction à @Versioned
pour empêcher la valeur de .approved
après avoir pris la valeur .denied
, alors la meilleure option est fatalError()
, qui ne convient pas aux applications réelles.
class ExpenseReport { @Versioned var state: State = .submitted { willSet { if newValue == .approved, $state.timestampedValues.map { $0.1 }.contains(.denied) { fatalError("") } } } } var tripExpenses = ExpenseReport() tripExpenses.state = .denied tripExpenses.state = .approved
Idées similaires
@Audited
pour @Audited
accès à la propriété.@UserDefault
pour encapsuler le mécanisme de lecture et d'enregistrement des données dans UserDefaults
.
Limitations
Les propriétés ne peuvent pas lever d'exceptions
Comme déjà mentionné, les wrappers de propriété ne peuvent utiliser que quelques méthodes pour traiter les valeurs non valides:
- les ignorer;
- terminer l'application à l'aide de fatalError ().
Les propriétés encapsulées ne peuvent pas être marquées avec l'attribut `typealias`
L'exemple @UnitInterval
ci-dessus, dont la propriété est limitée par l'intervalle (0...1)
, ne peut pas être déclaré comme
typealias UnitInterval = Clamping(0...1)
Restriction à l'utilisation d'une composition de plusieurs wrappers de propriété
Composer des wrappers de propriétés n'est pas une opération commutative: l'ordre de la déclaration affectera le comportement. Prenons un exemple dans lequel la propriété slug, qui est l'url d'un article de blog, est normalisée. Dans ce cas, le résultat de la normalisation variera selon le moment où les espaces seront remplacés par des tirets, avant ou après la suppression des espaces. Par conséquent, pour le moment, une composition de plusieurs wrappers de propriété n'est pas prise en charge.
@propertyWrapper struct Dasherized { private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.replacingOccurrences(of: " ", with: "-") } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { … @Dasherized @Trimmed var slug: String
Cependant, cette limitation peut être contournée en utilisant des wrappers de propriété imbriqués.
@propertyWrapper struct TrimmedAndDasherized { @Dasherized private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { … @TrimmedAndDasherized var slug: String }
Autres restrictions de wrapper de propriété
- Ne peut pas être utilisé à l'intérieur du protocole.
- Une instance de propriété wrapper ne peut pas être déclarée dans
enum
. - Une propriété encapsulée déclarée à l'intérieur d'une classe ne peut pas être remplacée par une autre propriété.
- Une propriété
@NSCopying
ne peut pas être lazy
, @NSCopying
, @NSManaged
, weak
ou sans unowned
. - Une propriété
@Lazy var (x, y) = /* ... */
doit être la seule dans sa définition (c'est-à-dire que @Lazy var (x, y) = /* ... */
). - Une propriété encapsulée ne peut pas avoir de
getter
et de setter
définis. - Les types de la propriété
wrappedValue
et de la variable wrappedValue
dans init(wrappedValue:)
doivent avoir le même niveau d'accès que le type d'encapsuleur de la propriété. - La propriété type de
projectedValue
doit avoir le même niveau d'accès que le type wrapper de la propriété. init()
doit avoir le même niveau d'accès que le type d'encapsuleur de propriété.
Résumons. Les wrappers de propriétés dans Swift permettent aux auteurs de bibliothèques d'accéder au comportement de haut niveau précédemment réservé aux fonctions de langage. Leur potentiel pour améliorer la lisibilité et réduire la complexité du code est énorme, et nous n'avons examiné que superficiellement les capacités de cet outil.
Utilisez-vous des wrappers de propriétés dans vos projets? Écrivez dans les commentaires!