Cara Mendekati Pembungkus untuk Swift Properties

Diterbitkan: 2022-03-11

Dalam istilah sederhana, pembungkus properti adalah struktur generik yang merangkum akses baca dan tulis ke properti dan menambahkan perilaku tambahan ke dalamnya. Kami menggunakannya jika kami perlu membatasi nilai properti yang tersedia, menambahkan logika ekstra ke akses baca/tulis (seperti menggunakan database atau default pengguna), atau menambahkan beberapa metode tambahan.

Pembungkus Properti di Swift 5.1

Artikel ini adalah tentang pendekatan Swift 5.1 baru untuk membungkus properti, yang memperkenalkan sintaks baru yang lebih bersih.

Pendekatan Lama

Bayangkan Anda sedang mengembangkan aplikasi, dan Anda memiliki objek yang berisi data profil pengguna.

 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)

Anda ingin menambahkan verifikasi email—jika alamat email pengguna tidak valid, properti email harus nil . Ini akan menjadi kasus yang baik untuk menggunakan pembungkus properti untuk merangkum logika ini.

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

Kita dapat menggunakan pembungkus ini dalam struktur Akun:

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

Sekarang, kami yakin bahwa properti email hanya dapat berisi alamat email yang valid.

Semuanya terlihat bagus, kecuali sintaks.

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

Dengan pembungkus properti, sintaks untuk menginisialisasi, membaca, dan menulis properti tersebut menjadi lebih kompleks. Jadi, apakah mungkin untuk menghindari komplikasi ini dan menggunakan pembungkus properti tanpa perubahan sintaks? Dengan Swift 5.1, jawabannya adalah ya.

Cara Baru: Anotasi @propertyWrapper

Swift 5.1 memberikan solusi yang lebih elegan untuk membuat pembungkus properti, di mana menandai pembungkus properti dengan anotasi @propertyWrapper diperbolehkan. Pembungkus seperti itu memiliki sintaks yang lebih ringkas dibandingkan dengan yang tradisional, menghasilkan kode yang lebih ringkas dan mudah dipahami. Anotasi @propertyWrapper hanya memiliki satu persyaratan: Objek pembungkus Anda harus berisi properti non-statis yang disebut 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) } }

Untuk mendefinisikan properti yang dibungkus seperti itu dalam kode, kita perlu menggunakan sintaks baru.

 @Email var email: String?

Jadi, kami menandai properti dengan anotasi @ . Jenis properti harus cocok dengan jenis pembungkus `wrappedValue`. Sekarang, Anda dapat bekerja dengan properti ini seperti yang biasa.

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

Bagus, sekarang terlihat lebih baik daripada dengan pendekatan lama. Tetapi implementasi wrapper kami memiliki satu kelemahan: Itu tidak mengizinkan nilai awal untuk nilai yang dibungkus.

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

Untuk mengatasi ini, kita perlu menambahkan penginisialisasi berikut ke pembungkus:

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

Dan itu saja.

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

Kode terakhir dari pembungkus di bawah ini:

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

Pembungkus yang Dapat Dikonfigurasi

Mari kita ambil contoh lain. Anda sedang menulis permainan, dan Anda memiliki properti tempat skor pengguna disimpan. Persyaratannya adalah nilai ini harus lebih besar dari atau sama dengan 0 dan kurang dari atau sama dengan 100. Anda dapat mencapainya dengan menggunakan pembungkus properti.

 @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

Kode ini berfungsi tetapi sepertinya tidak umum. Anda tidak dapat menggunakannya kembali dengan batasan yang berbeda (bukan 0 dan 100). Selain itu, hanya dapat membatasi nilai integer. Akan lebih baik untuk memiliki satu pembungkus yang dapat dikonfigurasi yang dapat membatasi jenis apa pun yang sesuai dengan protokol Sebanding. Untuk membuat pembungkus kita dapat dikonfigurasi, kita perlu menambahkan semua parameter konfigurasi melalui penginisialisasi. Jika penginisialisasi berisi atribut wrappedValue (nilai awal properti kita), itu harus menjadi parameter pertama.

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

Untuk menginisialisasi properti yang dibungkus, kami mendefinisikan semua atribut konfigurasi dalam tanda kurung setelah anotasi.

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

Jumlah atribut konfigurasi tidak terbatas. Anda perlu mendefinisikannya dalam tanda kurung dengan urutan yang sama seperti pada penginisialisasi.

Mendapatkan Akses ke Pembungkus Itu Sendiri

Jika Anda memerlukan akses ke pembungkus itu sendiri (bukan nilai yang dibungkus), Anda perlu menambahkan garis bawah sebelum nama properti. Sebagai contoh, mari kita ambil struktur Akun kita.

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

Kami membutuhkan akses ke pembungkus itu sendiri untuk menggunakan fungsionalitas tambahan yang kami tambahkan ke dalamnya. Misalnya, kami ingin struktur Akun sesuai dengan protokol Equatable. Dua akun sama jika alamat emailnya sama, dan alamat email harus peka huruf besar/kecil.

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

Ini berhasil, tetapi ini bukan solusi terbaik karena kita harus ingat untuk menambahkan metode huruf kecil() di mana pun kita membandingkan email. Cara yang lebih baik adalah dengan membuat struktur Email dapat disamakan:

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

dan bandingkan pembungkus alih-alih nilai yang dibungkus:

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

Nilai Proyeksi

Anotasi @propertyWrapper menyediakan satu gula sintaks lagi - nilai yang diproyeksikan. Properti ini dapat memiliki tipe apa pun yang Anda inginkan. Untuk mengakses properti ini, Anda perlu menambahkan awalan $ ke nama properti. Untuk menjelaskan cara kerjanya, kami menggunakan contoh dari kerangka Combine.

Pembungkus properti @Published membuat penerbit untuk properti dan mengembalikannya sebagai nilai yang diproyeksikan.

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

Seperti yang Anda lihat, kami menggunakan pesan untuk mengakses properti yang dibungkus, dan $message untuk mengakses penerbit. Apa yang harus Anda lakukan untuk menambahkan nilai yang diproyeksikan ke pembungkus Anda? Tidak ada yang istimewa, nyatakan saja.

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

Seperti disebutkan sebelumnya, properti projectedValue dapat memiliki tipe apa pun berdasarkan kebutuhan Anda.

Keterbatasan

Sintaks pembungkus properti baru terlihat bagus tetapi juga mengandung beberapa batasan, yang utama adalah:

  1. Mereka tidak dapat berpartisipasi dalam penanganan kesalahan. Nilai yang dibungkus adalah properti (bukan metode), dan kami tidak dapat menandai pengambil atau penyetel sebagai throws . Misalnya, dalam contoh Email kami, tidak mungkin membuat kesalahan jika pengguna mencoba menyetel email yang tidak valid. Kami dapat mengembalikan nil atau membuat aplikasi mogok dengan panggilan fatalError() , yang dalam beberapa kasus mungkin tidak dapat diterima.
  2. Menerapkan beberapa pembungkus ke properti tidak diperbolehkan. Misalnya, akan lebih baik untuk memiliki pembungkus @CaseInsensitive yang terpisah dan menggabungkannya dengan pembungkus @Email daripada membuat pembungkus @Email menjadi tidak peka huruf besar/kecil. Tetapi konstruksi seperti ini dilarang dan menyebabkan kesalahan kompilasi.
 @CaseInsensitive @Email var email: String?

Sebagai solusi untuk kasus khusus ini, kita dapat mewarisi pembungkus Email dari pembungkus CaseInsensitive . Namun, pewarisan juga memiliki batasan—hanya kelas yang mendukung pewarisan, dan hanya satu kelas dasar yang diizinkan.

Kesimpulan

@propertyWrapper penjelasan menyederhanakan sintaks pembungkus properti, dan kami dapat beroperasi dengan properti yang dibungkus dengan cara yang sama seperti yang biasa. Ini membuat kode Anda, sebagai Pengembang Swift lebih ringkas dan dapat dimengerti. Pada saat yang sama, ia memiliki beberapa batasan yang harus kita perhitungkan. Saya berharap beberapa dari mereka akan diperbaiki di versi Swift mendatang.

Jika Anda ingin mempelajari lebih lanjut tentang properti Swift, lihat dokumen resmi.