Cum să abordați Wrapper-urile pentru Swift Properties

Publicat: 2022-03-11

În termeni simpli, un wrapper de proprietate este o structură generică care încapsulează accesul de citire și scriere la proprietate și îi adaugă un comportament suplimentar. Îl folosim dacă trebuie să constrângem valorile proprietăților disponibile, să adăugăm o logică suplimentară accesului de citire/scriere (cum ar fi utilizarea bazelor de date sau a setărilor implicite de utilizator) sau să adăugăm câteva metode suplimentare.

Învelișuri de proprietate în Swift 5.1

Acest articol este despre o nouă abordare Swift 5.1 a proprietăților de împachetare, care introduce o nouă sintaxă mai curată.

Vechea abordare

Imaginați-vă că dezvoltați o aplicație și aveți un obiect care conține date de profil de utilizator.

 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)

Doriți să adăugați verificarea e-mailului — dacă adresa de e-mail a utilizatorului nu este validă, proprietatea de email -mail trebuie să fie nil . Acesta ar fi un caz bun pentru a utiliza un wrapper de proprietate pentru a încapsula această logică.

 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) } }

Putem folosi acest wrapper în structura contului:

 struct Account { var firstName: String var lastName: String var email: Email<String> }

Acum, suntem siguri că proprietatea de e-mail poate conține doar o adresă de e-mail validă.

Totul arată bine, cu excepția sintaxei.

 let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)

Cu un wrapper de proprietăți, sintaxa pentru inițializare, citire și scriere a unor astfel de proprietăți devine mai complexă. Deci, este posibil să evitați această complicație și să utilizați pachete de proprietăți fără modificări de sintaxă? Cu Swift 5.1, răspunsul este da.

Noua cale: @propertyWrapper Adnotare

Swift 5.1 oferă o soluție mai elegantă pentru crearea de pachete de proprietăți, unde este permisă marcarea unui pachet de proprietăți cu o adnotare @propertyWrapper . Astfel de wrapper-uri au o sintaxă mai compactă în comparație cu cele tradiționale, rezultând un cod mai compact și mai ușor de înțeles. Adnotarea @propertyWrapper are o singură cerință: obiectul wrapper trebuie să conțină o proprietate non-statică numită 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) } }

Pentru a defini o astfel de proprietate încapsulată în cod, trebuie să folosim noua sintaxă.

 @Email var email: String?

Deci, am marcat proprietatea cu adnotarea @ . Tipul de proprietate trebuie să se potrivească cu tipul de wrapper „wrappedValue”. Acum, puteți lucra cu această proprietate ca și cu cea obișnuită.

 email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil

Grozav, arată mai bine acum decât cu vechea abordare. Dar implementarea noastră wrapper are un dezavantaj: nu permite o valoare inițială pentru valoarea wrapper.

 @Email var email: String? = "[email protected]" //compilation error.

Pentru a rezolva acest lucru, trebuie să adăugăm următorul inițializator la wrapper:

 init(wrappedValue value: Value?) { self.value = value }

Si asta e.

 @Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil

Codul final al ambalajului este mai jos:

 @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) } }

Învelișuri configurabile

Să luăm un alt exemplu. Scrieți un joc și aveți o proprietate în care sunt stocate scorurile utilizatorului. Cerința este ca această valoare să fie mai mare sau egală cu 0 și mai mică sau egală cu 100. Puteți realiza acest lucru utilizând un wrapper de proprietate.

 @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

Acest cod funcționează, dar nu pare generic. Nu îl puteți reutiliza cu constrângeri diferite (nu 0 și 100). În plus, poate constrânge numai valori întregi. Ar fi mai bine să aveți un wrapper configurabil care poate constrânge orice tip care se conformează protocolului Comparable. Pentru a face wrapper-ul nostru configurabil, trebuie să adăugăm toți parametrii de configurare printr-un inițializator. Dacă inițializatorul conține un atribut wrappedValue (valoarea inițială a proprietății noastre), acesta trebuie să fie primul parametru.

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

Pentru a inițializa o proprietate încapsulată, definim toate atributele de configurare în paranteze după adnotare.

 @Constrained(0...100) var scores: Int = 0

Numărul de atribute de configurare este nelimitat. Trebuie să le definiți în paranteze în aceeași ordine ca și în inițializator.

Obținerea accesului la Wrapper în sine

Dacă aveți nevoie de acces la wrapper-ul în sine (nu la valoarea încapsulată), trebuie să adăugați un caracter de subliniere înainte de numele proprietății. De exemplu, să luăm structura contului nostru.

 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>)

Avem nevoie de acces la wrapper-ul în sine pentru a folosi funcționalitatea suplimentară pe care am adăugat-o. De exemplu, dorim ca structura contului să se conformeze protocolului Equatable. Două conturi sunt egale dacă adresele lor de e-mail sunt egale, iar adresele de e-mail nu trebuie să distingă majuscule și minuscule.

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }

Funcționează, dar nu este cea mai bună soluție, deoarece trebuie să ne amintim să adăugăm o metodă cu litere mici () oriunde comparăm e-mailurile. O modalitate mai bună ar fi să faceți structura de e-mail echivalentă:

 extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }

și comparați ambalajele în loc de valorile încapsulate:

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }

Valoarea proiectată

Adnotarea @propertyWrapper oferă încă un sugar de sintaxă - o valoare proiectată. Această proprietate poate avea orice tip doriți. Pentru a accesa această proprietate, trebuie să adăugați un prefix $ la numele proprietății. Pentru a explica cum funcționează, folosim un exemplu din cadrul Combine.

Wrapper-ul de proprietate @Published creează un editor pentru proprietate și îl returnează ca valoare proiectată.

 @Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher

După cum puteți vedea, folosim un mesaj pentru a accesa proprietatea înfășurată și un $message pentru a accesa editorul. Ce ar trebui să faci pentru a adăuga o valoare proiectată ambalajului tău? Nimic special, doar declara.

 @propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }

După cum sa menționat mai devreme, proprietatea projectedValue poate avea orice tip în funcție de nevoile dvs.

Limitări

Sintaxa noilor wrapper-uri de proprietăți arată bine, dar conține și câteva limitări, principalele fiind:

  1. Ei nu pot participa la tratarea erorilor. Valoarea încapsulată este o proprietate (nu o metodă) și nu putem marca getter-ul sau setter-ul ca throws . De exemplu, în exemplul nostru de e- Email , nu este posibil să aruncăm o eroare dacă un utilizator încearcă să seteze un e-mail nevalid. Putem returna nil sau bloca aplicația cu un apel fatalError() , care ar putea fi inacceptabil în unele cazuri.
  2. Nu este permisă aplicarea mai multor ambalaje pe proprietate. De exemplu, ar fi mai bine să aveți un wrapper @CaseInsensitive separat și să îl combinați cu un wrapper @Email în loc să faceți ca wrapper-ul @Email să nu țină seama de majuscule și minuscule. Dar construcții ca acestea sunt interzise și conduc la erori de compilare.
 @CaseInsensitive @Email var email: String?

Ca o soluție pentru acest caz particular, putem moșteni wrapper-ul de e- Email din wrapper-ul CaseInsensitive . Cu toate acestea, moștenirea are și limitări - numai clasele acceptă moștenirea și este permisă o singură clasă de bază.

Concluzie

Adnotările @propertyWrapper simplifică sintaxa wrapper-urilor de proprietăți și putem opera cu proprietățile încapsulate în același mod ca și cu cele obișnuite. Acest lucru face ca codul dvs., ca dezvoltator Swift, să fie mai compact și mai ușor de înțeles. În același timp, are câteva limitări de care trebuie să ținem cont. Sper că unele dintre ele vor fi rectificate în viitoarele versiuni Swift.

Dacă doriți să aflați mai multe despre proprietățile Swift, consultați documentele oficiale.