Property Wrapper in Swift
In SwiftUI ist die Verwendung von Property Wrappern wie @State gang und gäbe. Werfen wir einen Blick hinter die Kulissen: Wie genau funktionieren Property Wrapper und wie kann man sie selbst definieren?
Tutorial
-
Lade den Start-Stand des Beispielprojektes PropertyWrapperExample herunter.
-
Führe in dem Projekt die Unit-Tests via Product » Test ⌘U aus. Diese prüfen, ob die Eigenschaft percentValue im Wertebereich von 0...100 bleibt (wird ein Wert kleiner 0 gesetzt, soll stattdessen 0 verwendet werden usw.). Diese schlagen im Moment fehl, da die entsprechende Implementierung fehlt:
-
Ergänze in ExampleModel einen didSet-Block, und implementiere eine Prüfung auf den Wertebereich. Führe die Tests aus, um zu überprüfen, ob die Implementierung korrekt funktioniert.
struct ExampleModel { var percentValue = 5.0 { didSet { if self.percentValue < 0 { self.percentValue = 0 } else if self.percentValue > 100 { self.percentValue = 100 } } } }
-
Mit einem Property-Wrapper kann diese Prüfung in wiederverwendbarer Form extrahiert werden und für beliebige Properties verwendet werden.
Implementiere dazu ein struct und deklariere dieses als @propertyWrapper. Implementiere eine Eigenschaft wrappedValue - an diese wird der Zugriff auf die Eigenschaft weitergeleitet. Verschiebe den didSet-Block zu dieser Eigenschaft. Verwende den Property Wrapper im ExampleModel:
@propertyWrapper struct PercentValue { var wrappedValue: Float { didSet { if self.wrappedValue < 0 { self.wrappedValue = 0 } else if self.wrappedValue > 100 { self.wrappedValue = 100 } } } } struct ExampleModel { @PercentValue var percentValue: Float }
-
Der Zugriff auf eine Eigenschaft, die mit einem Property-Wrapper versehen wurde (percentValue im Beispiel), wird zu dem wrappedValue-Property im Property Wrapper weiterdelegiert. Der Compiler wird intern etwa folgenden Code erzeugen, um dies umzusetzen:
struct ExampleModel { var _percentValue : PercentValue var percentValue : Float { get { _percentValue.wrappedValue } set { _percentValue.wrappedValue = newValue } } }
Zusatzaufgaben
-
Die Logik des Property-Wrappers lässt sich noch etwas kürzer schreiben - implementiere die Werteprüfung mit den min/max-Funktionen und überprüfe diese mit den Unit-Tests.
var wrappedValue: Float { didSet { self.wrappedValue = min(100, max(0, self.wrappedValue)) } }
-
Ergänze den Property Wrapper so, dass beliebige Wertebereiche unterstützt werden. Benenne dazu den Property Wrapper in Clamp um. Füge eine Eigenschaft range : ClosedRange<Float> hinzu und generiere mit Refactor » Generate Memberwise initializer einen Initializer (beachte die Reihenfolge der Argumente: zuerst wrappedValue, dann range). Verwende die Eigenschaften lowerBound und upperBound des Wertebereichs für die Implementierung.
@propertyWrapper struct Clamp { init(wrappedValue: Float, range: ClosedRange<Float>) { self.wrappedValue = wrappedValue self.range = range } var wrappedValue: Float { didSet { self.wrappedValue = min(100, max(0, self.wrappedValue)) } } let range: ClosedRange<Float> } struct ExampleModel { @Clamp(range: 0...100) var percentValue = 5.0 }
-
Ersetze die Verwendung des Typs Float durch ein generisches Argument <Value : Comparable> um beliebige Typen zu unterstützen (diese müssen lediglich vergleichbar sein, d.h. konform zu dem Comparable-Protokoll sein):
@propertyWrapper struct Clamp <Value: Comparable> { init(wrappedValue: Value, range: ClosedRange<Value>) { self.wrappedValue = wrappedValue self.range = range } var wrappedValue: Value { didSet { self.wrappedValue = min(range.upperBound, max(range.lowerBound, self.wrappedValue)) } } let range: ClosedRange<Value> } struct ExampleModel { @Clamp(0...100) var percentValue: Float = 0 @Clamp(0...100) var intValue: Int = 0 }
-
Seit Swift 5.5 / Xcode 13 können Property Wrapper auch für Variablen und Argumente verwendet werden. Probiere dies in einer neuen Unit-Test-Methode aus und implementiere den Property-Wrapper so, dass der Wertebereich auch für den initial gesetzten Wert erzwungen wird.
func testClampVariable() { @Clamp(range: 0 ... 10) var value = -1 XCTAssertEqual(0, value) value = 11 XCTAssertEqual(10, value) }
Weitere Informationen
Beispiele für Property Wrapper
-
ValidatedPropertyKit: Validate your Properties with Property WrappersValidatedPropertyKit stellt einen Property-Wrapper @Validated für Validierungsregeln zur Verfügung.
-
Burritos: A collection of Swift Property WrappersSammlung von Property Wrappern, z.B. @Trimmed, @Clamped, @AtomicWrite.
-
Swift Property Wrapper for LoggingBeispiel für einen Property-Wrapper, der alle Änderungen an einem Wert auf der Konsole ausgibt.
Darüber hinaus
-
Projecting a Value From a Property WrapperEin Property Wrapper kann eine projectedValue-Eigenschaft deklarieren - der Zugriff auf diese erfolgt dann über $property.
-
Accessing a Swift property wrapper’s enclosing instanceÜber Keypaths kann ein Property Wrapper auf das Objekt, das ihn beinhaltet, zugreifen (dies wird verwendet für @Published, der auf das objectWillChange-Property zugreift).
-
Nested Property Wrappers in SwiftEin grundsätzliches, ungelöstetes Problem bei den Property-Wrappern: Mehrere Property-Wrapper sind schwierig in Kombination zu verwenden (bspw. @Published und @Clamp zusammen).