Die in Swift 5.1 eingeführten Property Wrapper wurden schnell zu einer der am häufigsten verwendeten Funktionen in allen Swift-bezogenen Bereichen. Das heute berühmte SwiftUI-Framework wäre wahrscheinlich nicht das, was es heute ist, ohne @State
, @Binding
, @StateObject
und andere schwarze Zaubermittel, die Sie täglich verwenden. Oder vielleicht haben Sie von einem der beliebtesten Object Relational Mapper Frameworks (ORM) für ein serverseitiges Swift's Vapor namens Fluent gehört oder es sogar selbst verwendet. Sie müssen sich nicht entscheiden, wie und wann Sie ein Dollarzeichen $
vor einer Eigenschaft verwenden, die ordentlich in einen weiteren Wrapper verpackt ist.
Die Wahrheit ist, dass Property Wrapper gar nicht so schwer zu knacken sind, wenn man sich gründlich damit befasst und versucht, sie selbst zu verwenden. Tatsächlich gibt es in Ihrem Projekt wahrscheinlich eine Reihe von Stellen, an denen Sie die Dinge einfacher machen können, indem Sie einen Wrapper zu Hilfe nehmen. In diesem Leitfaden behandele ich 5 grundlegende Anwendungsfälle wie diesen. Aber lassen Sie uns zunächst unser Gedächtnis zum Thema selbst auffrischen.
Ein Property Wrapper ist ein speziell annotierter Typ, der eine Logik kapselt, die auf alle Eigenschaften im Allgemeinen oder auf Eigenschaften angewendet wird, die auf einen bestimmten Typ beschränkt sind, den Sie auswählen. Es kann sich um eine Struktur, Aufzählung oder Klasse handeln, die mit einem @propertyWrapper
Attribut deklariert ist und mindestens einen wrappedValue
als obligatorische Eigenschaft enthält:
@propertyWrapper struct SomeWrapper<T> { var wrappedValue: T { // mandatory property get { value } set { value = newValue } } private var value: T init(wrappedValue: T) { self.value = wrappedValue } }
Sowohl in einem Getter als auch in einem Setter der umschlossenen Werteigenschaft können Sie jede gewünschte Logik hinzufügen und Werte grundsätzlich auf jede gewünschte Weise abfangen, transformieren oder verwenden. Anschließend können Sie den Wrapper folgendermaßen verwenden:
class SomeService { @SomeWrapper fancyNumber: Int init(number: Int) { self._number = SomeWrapper(wrappedValue: number) // use "_" to initialize a property wrapper } func doSmth() { print(fancyNumber) } }
Das Schöne an Property Wrappern ist, dass Sie Ihre eigenen Initialisierer, zusätzlichen Methoden, Eigenschaften usw. hinzufügen können, die alle von Ihnen benötigten Funktionen haben. Eine davon ist eine optionale projectedValue
Eigenschaft, die sogar einen Wrapper selbst oder einen anderen Typ zurückgeben kann. Denken Sie daran: Wenn Sie auf etwas zugreifen möchten, das zum Wrapper selbst gehört, aber nicht zu seinem Wert, müssen Sie ein berüchtigtes Dollarzeichen verwenden:
@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 } }
Natürlich veranschaulichen die oben aufgeführten unsinnigen Beispiele die wahre Leistungsfähigkeit von Property Wrappern nicht ausreichend. Schauen wir uns also einige der nützlichsten Möglichkeiten an, um sie optimal zur Geltung zu bringen.
Inspiriert von all den Dingen, die Fluent kann, können wir Property Wrapper anwenden, um mit UserDefaults
umzugehen, anstatt einen separaten Dienst dafür zu erstellen:
@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) } } }
Wie Sie sehen, kann der Wrapper oben mit allem verwendet werden, was Codable
@DefaultsStorage(key: "upload_date", defaultValue: Date()) var uploadDate: Date
Die obige Implementierung kann je nach Bedarf stark optimiert und geändert werden. Sie können sie beispielsweise wirklich vielseitig machen, indem Sie andere Typen akzeptieren oder die Verwendung von Suite-basierten UserDefaults
anstelle des vordefinierten standard
-Singletons zulassen.
Eine weitere Sache sind die Daten, die Sie damit speichern. Wenn Sie etwas sicher speichern möchten, möchten Sie möglicherweise eine ähnliche Lösung für Keychain schreiben.
Apple verfügt übrigens bereits über einen ähnlichen Wrapper namens @AppStorage
, der allerdings für die Verwendung mit SwiftUI konzipiert wurde, was Sie möglicherweise nicht unbedingt möchten.
In Fällen, in denen Sie einen bestimmten Wert überwachen müssen, kann der folgende Eigenschaften-Wrapper für Sie hilfreich sein:
@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)
Dieser Property Wrapper kann durch die Einbindung zusätzlicher Analysedienste noch nützlicher werden. Dies kann lokal oder sogar remote erfolgen. Wenn Sie Letzteres verwenden, denken Sie daran, an der Aufrufstelle eines solchen Dienstes zu filtern, um unnötige Aufrufe zu vermeiden.
Wir müssen Benutzereingaben häufig validieren. Es gibt so viele mögliche Eingabetypen, wie E-Mail, Telefonnummer, Geburtsdatum usw. Alle diese erfordern eine bestimmte Logik, die auf die Originaldaten angewendet wird. Sie können dies lösen, indem Sie für jede Eingabe einen Eigenschaftenwrapper verwenden:
@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) } }
Oder Sie können sogar eine Einheitslösung wie diese ausprobieren:
@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: ... } } }
Ähnlich wie bei der Validierung können Sie Datumsangaben mit Eigenschaften-Wrappern formatieren, indem Sie eine Reihe vorangestellter Formate definieren, die Sie innerhalb des Wrappers verwenden würden.
Manchmal müssen Sie einen Wert in einem bestimmten Bereich begrenzen, z. B. bei einem Prozentwert von 0 bis 100 oder einem Bruchwert von 0 bis 1 usw. Sie können diese Logik auch in einem Eigenschaftenwrapper kapseln:
@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) } } }
Sie können es folgendermaßen verwenden:
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 } }
Ein weiterer Anwendungsfall für Property Wrapper ist die Unterstützung bei allen Dingen, Codable
. Benutzerdefinierte Implementierungen der Kodierungs- und Dekodierungslogik können bei Bedarf sicher in einen Wrapper eingefügt werden. Mit dieser einfachen Lösung können Sie einen String auch automatisch in Base64 kodieren:
@propertyWrapper struct Base64Encoding { private var value = "" var wrappedValue: String { get { Data(value.utf8).base64EncodedString() } set { value = newValue } } }
Wenn Sie mit dem oben stehenden Wrapper auf den von ihm transformierten Text zugreifen, wird immer eine Base64-codierte Zeichenfolge zurückgegeben.
Vergessen Sie beim Umgang mit Property Wrappern jedoch nicht, dass es sich um dieselben Swift-Basistypen handelt, die wir seit Jahren kennen, aber mit zusätzlichen Funktionen. Eine der vielen beliebten Ideen zur Verwendung eines Property Wrappers in der Vergangenheit war der Versuch, damit threadsichere Eigenschaften zu erstellen. Sie können online mehrere Implementierungen von @Atomic
finden, aber es hat sich herausgestellt, dass die meisten davon ziemlich problematisch sind . Obwohl es auch Fixes dafür gibt, empfehle ich nicht mehr, @Atomic
zu verwenden, insbesondere da jetzt Akteure für diesen Zweck verfügbar sind.
Ich hoffe, Sie haben viel gelernt oder zumindest Ihr Wissen über Property Wrapper in Swift aufgefrischt. Werfen Sie mit dieser Auffrischung einen Blick auf Ihre aktuellen Projekte: Vielleicht haben Sie einige Teile, die die Leistungsfähigkeit dieser Funktion nutzen können!