Property Wrapper in Swift

von @ralfebert · aktualisiert am 20. November 2021
Xcode 13 & iOS 15
Fortgeschrittene iOS-Entwickler*innen
Deutsch

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

  1. Lade den Start-Stand des Beispielprojektes PropertyWrapperExample herunter.

  2. 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:

  3. 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.

    Lösung anzeigen

    struct ExampleModel {
        var percentValue = 5.0 {
            didSet {
        if self.percentValue < 0 {
            self.percentValue = 0
        }
        else if self.percentValue > 100 {
            self.percentValue = 100
        }
    }
        }
    }
    
  4. 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
    }
    
  5. 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

  1. 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.

    Lösung anzeigen

    var wrappedValue: Float {
        didSet {
            self.wrappedValue = min(100, max(0, self.wrappedValue))
        }
    }
    
  2. 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.

    Lösung anzeigen

    @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
    }
    
  3. 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):

    Lösung anzeigen

    @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
    }
    
  4. 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.

    Lösung anzeigen

    func testClampVariable() {
        @Clamp(range: 0 ... 10) var value = -1
        XCTAssertEqual(0, value)
        value = 11
        XCTAssertEqual(10, value)
    }
    

Weitere Informationen

Beispiele für Property Wrapper

Darüber hinaus