Property Wrapper in Swift 5.1 – Teil 1

Was sie sind und wie man sie erstellt

Die sogenannten Property Wrapper gehören zu den spannendsten neuen Features in Swift 5.1. Da sie im Vergleich zu den kleineren Neuerungen und Änderungen von Swift 5.1 aber deutlich komplexer sind, habe ich mich dazu entschieden, ihnen eine eigene kleine Artikelserie hier auf dem Blog zu gönnen. Das erlaubt es mir, euch die Property Wrapper detailliert und ausführlich vorzustellen und ihre Funktionsweise zu erläutern.

Zunächst einmal widme ich mich der grundlegendsten aller Fragen: Was sind Property Wrapper überhaupt, und wofür sind sie gut?

Mithilfe von Property Wrappern definiert man eine Management-Logik, die sich auf beliebige Properties anwenden lässt. Diese Logik kann beispielsweise auf eine Datenbank zugreifen, um bei Zuweisung eines Werts zu einer Property diesen in eine passende Tabelle zu schreiben.

In solch einem Szenario musste man bisher entweder Hilfsfunktionen erstellen oder die Management-Logik in allen Properties wiederholen, denen identische Anforderungen zugrundeliegen. Hat man so beispielsweise mehrere Properties, die mit einem Eintrag in einer Datenbank gekoppelt sind, muss für jede dieser Properties der Datenbankzugriff geregelt werden, auch wenn der immer auf die gleiche Art und Weise erfolgt.

Dieser Problematik nehmen sich die Property Wrapper an. Sie definieren, was beim Lesen und Schreiben von Properties geschehen soll und erlauben so einen Eingriff in diese Prozesse. Einmal erstellt, kann ein Property Wrapper so auf beliebig viele Properties angewendet werden.

Erstellen eines Property Wrappers

Wechseln wir nun von der Theorie in die Praxis und erstellen einen ersten Property Wrapper! Basis eines solchen ist zunächst ein eigens erstellter Typ auf Basis einer Enumeration, Structure oder Klasse. Um ihn als Property Wrapper verwenden zu können, müssen zwei Anforderungen erfüllt werden:

  1. Der Typ muss mithilfe des Schlüsselworts @propertyWrapper deklariert werden.
  2. Innerhalb des Typs muss eine Property namens wrappedValue implementiert werden.

Welchem Typ die wrappedValue-Property entspricht, ist davon abhängig, für welche Art von Properties der Property Wrapper eingesetzt werden kann. Kümmert der sich beispielsweise um ein spezielles Management für Strings, sollte die wrappedValue-Property ebenso dem Typ String entsprechen.

Daneben können auf Wunsch beliebige weitere Properties und Methoden innerhalb eines Property Wrappers definiert werden. So lässt sich die gesamte Logik umsetzen, die ein Property Wrapper benötigt.

Weist man einer Property einen Property Wrapper zu, wird beim Lesen und Schreiben jener Property auf wrappedValue zugegriffen. wrappedValue bestimmt also, welchen Wert die Property zurückgibt und was beim Setzen eines neuen Werts zu tun ist.

Das folgende Listing zeigt als Beispiel einen simplen Property Wrapper namens SmallNumber. Er dient dazu, nur Integer-Werte zu speichern, die nicht größer sind als 9. Die entsprechende Logik findet sich in der Implementierung der wrappedValue-Property (bei der es sich in diesem Fall passenderweise um einen Integer handelt). Damit die Implementierung gelingt, besitzt SmallNumber noch eine Hilfs-Property namens currentNumber, die den aktuellen Wert speichert. Der wird beim Zugriff auf wrappedValue zurückgegeben. Bei Zuweisung eines neuen Werts prüft der Setter von wrappedValue, ob der neue Wert kleiner gleich 9 ist. Falls ja, wird er gespeichert, andernfalls wird currentNumber schlicht der erlaubte Höchstwert 9 zugewiesen.

@propertyWrapper
struct SmallNumber {
    
    private var currentNumber = 0
    
    var wrappedValue: Int {
        get {
            return currentNumber
        }
        set {
            if newValue <= 9 {
                currentNumber = newValue
            } else {
                currentNumber = 9
            }
        }
    }
    
}

Property Wrapper zuweisen und einsetzen

Möchte man nun diesen Property Wrapper einer Property zuweisen und verwenden, stellt man der gewünschten Property den Namen des Property Wrappers voran. Der wird dabei mit einem @-Zeichen eingeleitet. Möchte man also den eben erstellten Property Wrapper auf Basis von SmallNumber nutzen, spricht man diesen mittels @SmallNumber an. Ein Beispiel hierfür zeigt das folgende Listing:

struct SomeRectangle {

    @SmallNumber public var height: Int

    @SmallNumber public var width: Int

}

Verwendet man nun die mittels @SmallNumber deklarierten Properties, greift die Logik, die innerhalb des Property Wrappers definiert ist. Verdeutlicht wird das durch das Beispiel im folgenden Listing. Darin wird eine Instanz der SomeRectangle-Structure erstellt und den beiden Properties height und width jeweils ein passender Wert zugewiesen. Beim lesenden Zugriff auf diese Werte stellt man dann fest, dass der Wert von width statt 10 nur 9 entspricht; hier wurde schließlich das im Property Wrapper definierte Maximum überschritten.

var myRectangle = SomeRectangle()
myRectangle.height = 5
myRectangle.width = 10

print("myRectangle.height = \(myRectangle.height)")
// myRectangle.height = 5

print("myRectangle.width = \(myRectangle.width)")
// myRectangle.width = 9

Bei jedem Zugriff auf die height– und width-Property wird somit die Logik ausgeführt, die in der wrappedValue-Property des Property Wrappers definiert ist. Das hat zur Folge, das mit @SmallNumber deklarierte Properties niemals größer als 9 sein können.

Fazit

Mithilfe von Property Wrappern lässt sich eine gemeinsame Logik für Properties festlegen. Weist man einer Property einen solchen Property Wrapper zu, bestimmt der, was beim lesenden und schreibenden Zugriff für Befehle ausgeführt werden. So lässt sich Code deutlich flexibler und einheitlicher gestalten.

Euer Thomas


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert