Wie man sich Wrappern für Swift-Eigenschaften nähert
Veröffentlicht: 2022-03-11Einfach ausgedrückt ist ein Eigenschaftswrapper eine generische Struktur, die den Lese- und Schreibzugriff auf die Eigenschaft kapselt und ihr zusätzliches Verhalten hinzufügt. Wir verwenden es, wenn wir die verfügbaren Eigenschaftswerte einschränken, dem Lese-/Schreibzugriff zusätzliche Logik hinzufügen (z. B. die Verwendung von Datenbanken oder Benutzervorgaben) oder einige zusätzliche Methoden hinzufügen müssen.
Dieser Artikel handelt von einem neuen Swift 5.1-Ansatz zum Umschließen von Eigenschaften, der eine neue, sauberere Syntax einführt.
Alter Ansatz
Stellen Sie sich vor, Sie entwickeln eine Anwendung und haben ein Objekt, das Benutzerprofildaten enthält.
struct Account { var firstName: String var lastName: String var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email = "[email protected]" print(account.email)
Sie möchten eine E-Mail-Bestätigung hinzufügen – wenn die E-Mail-Adresse des Benutzers ungültig ist, muss die Eigenschaft email
nil
sein. Dies wäre ein guter Fall, um diese Logik mit einem Eigenschaften-Wrapper zu kapseln.
struct Email<Value: StringProtocol> { private var _value: Value? init(initialValue value: Value?) { _value = value } var value: Value? { get { return validate(email: _value) ? _value : nil } set { _value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}" let pred = NSPredicate(format: "SELF MATCHES %@", regex) return pred.evaluate(with: email) } }
Wir können diesen Wrapper in der Kontostruktur verwenden:
struct Account { var firstName: String var lastName: String var email: Email<String> }
Jetzt sind wir sicher, dass die E-Mail-Eigenschaft nur eine gültige E-Mail-Adresse enthalten kann.
Alles sieht gut aus, außer der Syntax.
let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)
Mit einem Eigenschaftswrapper wird die Syntax zum Initialisieren, Lesen und Schreiben solcher Eigenschaften komplexer. Ist es also möglich, diese Komplikation zu vermeiden und Eigenschafts-Wrapper ohne Syntaxänderungen zu verwenden? Mit Swift 5.1 lautet die Antwort ja.
Der neue Weg: @propertyWrapper-Anmerkung
Swift 5.1 bietet eine elegantere Lösung zum Erstellen von Property-Wrappern, bei der das Markieren eines Property-Wrappers mit einer @propertyWrapper
Anmerkung zulässig ist. Solche Wrapper haben im Vergleich zu den traditionellen Wrappern eine kompaktere Syntax, was zu einem kompakteren und verständlicheren Code führt. Die Annotation @propertyWrapper
hat nur eine Anforderung: Ihr Wrapper-Objekt muss eine nicht-statische Eigenschaft namens wrappedValue
.
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = 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) } }
Um eine solche verpackte Eigenschaft im Code zu definieren, müssen wir die neue Syntax verwenden.
@Email var email: String?
Also haben wir die Eigenschaft mit der Anmerkung @ markiert
email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil
Toll, es sieht jetzt besser aus als mit dem alten Ansatz. Aber unsere Wrapper-Implementierung hat einen Nachteil: Sie erlaubt keinen Anfangswert für den umschlossenen Wert.
@Email var email: String? = "[email protected]" //compilation error.
Um dies zu beheben, müssen wir dem Wrapper den folgenden Initialisierer hinzufügen:
init(wrappedValue value: Value?) { self.value = value }
Und das ist es.
@Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil
Der endgültige Code des Wrappers ist unten:
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = 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) } }
Konfigurierbare Wrapper
Nehmen wir ein anderes Beispiel. Sie schreiben ein Spiel und haben eine Eigenschaft, in der die Benutzerergebnisse gespeichert werden. Die Anforderung ist, dass dieser Wert größer oder gleich 0 und kleiner oder gleich 100 sein sollte. Sie können dies erreichen, indem Sie einen Eigenschafts-Wrapper verwenden.
@propertyWrapper struct Scores { private let minValue = 0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } } @Scores var scores: Int = 0
Dieser Code funktioniert, aber er scheint nicht generisch zu sein. Sie können es nicht mit anderen Einschränkungen (nicht 0 und 100) wiederverwenden. Darüber hinaus kann es nur ganzzahlige Werte einschränken. Es wäre besser, einen konfigurierbaren Wrapper zu haben, der jeden Typ einschränken kann, der dem Comparable-Protokoll entspricht. Um unseren Wrapper konfigurierbar zu machen, müssen wir alle Konfigurationsparameter über einen Initialisierer hinzufügen. Wenn der Initialisierer ein wrappedValue
Attribut (den Anfangswert unserer Eigenschaft) enthält, muss dies der erste Parameter sein.

@propertyWrapper struct Constrained<Value: Comparable> { private var range: ClosedRange<Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange<Value>) { self.value = value self.range = range } var wrappedValue: Value { get { return max(min(value, range.upperBound), range.lowerBound) } set { value = newValue } } }
Um eine umschlossene Eigenschaft zu initialisieren, definieren wir alle Konfigurationsattribute in Klammern nach der Anmerkung.
@Constrained(0...100) var scores: Int = 0
Die Anzahl der Konfigurationsattribute ist unbegrenzt. Sie müssen sie in Klammern in der gleichen Reihenfolge wie im Initialisierer definieren.
Zugriff auf den Wrapper selbst erhalten
Wenn Sie Zugriff auf den Wrapper selbst benötigen (nicht auf den umschlossenen Wert), müssen Sie vor dem Eigenschaftsnamen einen Unterstrich hinzufügen. Nehmen wir zum Beispiel unsere Kontostruktur.
struct Account { var firstName: String var lastName: String @Email var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email // Wrapped value (String) account._email // Wrapper(Email<String>)
Wir benötigen Zugriff auf den Wrapper selbst, um die zusätzlichen Funktionen zu nutzen, die wir ihm hinzugefügt haben. Beispielsweise möchten wir, dass die Kontostruktur dem Equatable-Protokoll entspricht. Zwei Konten sind gleich, wenn ihre E-Mail-Adressen gleich sind, und bei den E-Mail-Adressen muss die Groß-/Kleinschreibung nicht beachtet werden.
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }
Es funktioniert, aber es ist nicht die beste Lösung, weil wir daran denken müssen, überall dort, wo wir E-Mails vergleichen, eine Lowercased()-Methode hinzuzufügen. Ein besserer Weg wäre, die E-Mail-Struktur gleich zu machen:
extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }
und vergleichen Sie Wrapper anstelle von umschlossenen Werten:
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }
Voraussichtlicher Wert
Die Annotation @propertyWrapper
bietet einen weiteren Syntaxzucker – einen projizierten Wert. Diese Eigenschaft kann jeden gewünschten Typ haben. Um auf diese Eigenschaft zuzugreifen, müssen Sie dem Eigenschaftsnamen das Präfix $
hinzufügen. Um zu erklären, wie es funktioniert, verwenden wir ein Beispiel aus dem Combine-Framework.
Der Eigenschaftswrapper @Published
erstellt einen Herausgeber für die Eigenschaft und gibt ihn als prognostizierten Wert zurück.
@Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher
Wie Sie sehen können, verwenden wir eine Nachricht, um auf die umschlossene Eigenschaft zuzugreifen, und eine $message, um auf den Herausgeber zuzugreifen. Was sollten Sie tun, um Ihrem Wrapper einen prognostizierten Wert hinzuzufügen? Nichts Besonderes, einfach angeben.
@propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }
Wie bereits erwähnt, kann die Eigenschaft projectedValue
je nach Ihren Anforderungen einen beliebigen Typ haben.
Einschränkungen
Die Syntax der neuen Property-Wrapper sieht gut aus, enthält aber auch mehrere Einschränkungen, die wichtigsten sind:
- Sie können sich nicht an der Fehlerbehandlung beteiligen. Der verpackte Wert ist eine Eigenschaft (keine Methode), und wir können den Getter oder Setter nicht als
throws
markieren. In unserem E-Email
-Beispiel ist es beispielsweise nicht möglich, einen Fehler auszulösen, wenn ein Benutzer versucht, eine ungültige E-Mail-Adresse festzulegen. Wir könnennil
zurückgeben oder die App mit einemfatalError()
-Aufruf zum Absturz bringen, was in manchen Fällen nicht akzeptabel sein könnte. - Das Anwenden mehrerer Wrapper auf die Eigenschaft ist nicht zulässig. Beispielsweise wäre es besser, einen separaten
@CaseInsensitive
Wrapper zu haben und ihn mit einem@Email
Email-Wrapper zu kombinieren, anstatt den@Email
Wrapper unempfindlich gegen Groß- und Kleinschreibung zu machen. Aber Konstruktionen wie diese sind verboten und führen zu Kompilierungsfehlern.
@CaseInsensitive @Email var email: String?
Als Problemumgehung für diesen speziellen Fall können wir den E- Email
-Wrapper vom CaseInsensitive
Wrapper erben. Die Vererbung hat jedoch auch Einschränkungen – nur Klassen unterstützen die Vererbung, und es ist nur eine Basisklasse erlaubt.
Fazit
@propertyWrapper
-Annotationen vereinfachen die Syntax der Eigenschaftswrapper, und wir können mit den umschlossenen Eigenschaften genauso arbeiten wie mit den gewöhnlichen. Dadurch wird Ihr Code als Swift-Entwickler kompakter und verständlicher. Gleichzeitig hat es mehrere Einschränkungen, die wir berücksichtigen müssen. Ich hoffe, dass einige davon in zukünftigen Swift-Versionen behoben werden.
Wenn Sie mehr über Swift-Eigenschaften erfahren möchten, sehen Sie sich die offiziellen Dokumente an.