Jika Anda menggunakan SwiftUI, Anda mungkin memperhatikan kata kunci seperti @ObservedObject, @EnvironmentObject, @FetchRequest, dan sebagainya. Pembungkus Properti (selanjutnya disebut "pembungkus properti") adalah fitur baru dari Swift 5.1. Artikel ini akan membantu Anda memahami dari mana semua konstruksi dari @ berasal, bagaimana cara menggunakannya di SwiftUI dan dalam proyek Anda.

Diterjemahkan oleh: Evgeny Zavozhansky, pengembang FunCorp.
Catatan: Pada saat terjemahan disiapkan, bagian dari kode sumber artikel asli telah kehilangan relevansinya karena perubahan bahasa, sehingga beberapa contoh kode sengaja diganti.
Pembungkus properti pertama kali diperkenalkan di forum Swift pada Maret 2019, beberapa bulan sebelum pengumuman SwiftUI. Dalam proposal awalnya, Douglas Gregor, anggota tim Swift Core, menggambarkan konstruk ini (kemudian disebut delegasi properti) sebagai "generalisasi yang dapat diakses pengguna dari fungsi yang saat ini disediakan oleh konstruksi bahasa seperti lazy
, misalnya."
Jika sebuah properti dideklarasikan dengan kata kunci lazy
, ini berarti ia akan diinisialisasi saat pertama kali diakses. Sebagai contoh, inisialisasi properti ditangguhkan dapat diimplementasikan menggunakan properti pribadi, diakses melalui properti yang dihitung. Tetapi menggunakan kata kunci lazy
membuat ini lebih mudah.
struct Structure {
SE-0258: Property Wrapper dengan sempurna menjelaskan desain dan implementasi pembungkus properti. Karena itu, alih-alih mencoba meningkatkan deskripsi dalam dokumentasi resmi, pertimbangkan beberapa contoh yang dapat diimplementasikan menggunakan pembungkus properti:
- pembatasan nilai properti;
- konversi nilai saat mengubah properti;
- mengubah semantik persamaan dan membandingkan properti;
- logging akses properti.
Batasi Nilai Properti
SE-0258: Property Wrapper menyediakan beberapa contoh praktis, termasuk @Clamping
, @Copying
, @Atomic
, @ThreadSpecific
, @Box
, @UserDefault
. Pertimbangkan bungkus @Clamping
, yang memungkinkan Anda membatasi nilai maksimum atau minimum suatu properti.
@propertyWrapper struct Clamping<Value: Comparable> { var value: Value let range: ClosedRange<Value> init(initialValue value: Value, _ range: ClosedRange<Value>) { precondition(range.contains(value)) self.value = value self.range = range } var wrappedValue: Value { get { value } set { value = min(max(range.lowerBound, newValue), range.upperBound) } } }
@Clamping
dapat digunakan, misalnya, untuk mensimulasikan keasaman suatu larutan, yang nilainya dapat mengambil nilai dari 0 hingga 14.
struct Solution { @Clamping(0...14) var pH: Double = 7.0 } let carbonicAcid = Solution(pH: 4.68)
Mencoba menetapkan nilai pH di luar rentang dari (0...14)
akan menyebabkan properti mengambil nilai yang paling dekat dengan interval minimum atau maksimum.
let superDuperAcid = Solution(pH: -1) superDuperAcid.pH
Pembungkus properti dapat digunakan untuk mengimplementasikan pembungkus properti lainnya. Misalnya, pembungkus @UnitInterval
membatasi nilai properti hingga interval (0...1)
menggunakan @Clamping(0...1)
:
@propertyWrapper struct UnitInterval<Value: FloatingPoint> { @Clamping(0...1) var wrappedValue: Value = .zero init(initialValue value: Value) { self.wrappedValue = value } }
Ide serupa
@Positive
/ @NonNegative
menunjukkan bahwa nilainya dapat berupa angka positif atau negatif.@NonZero
menunjukkan bahwa nilai properti tidak boleh 0.@Validated
atau @Whitelisted
/ @Blacklisted
membatasi nilai properti ke nilai tertentu.
Mengkonversi Nilai Saat Mengubah Properti
Memvalidasi nilai-nilai bidang teks adalah sakit kepala konstan untuk pengembang aplikasi. Ada begitu banyak hal yang harus dilacak: mulai dari kata-kata hampa seperti pengodean hingga upaya jahat untuk memasukkan kode melalui bidang teks. Pertimbangkan untuk menggunakan pembungkus properti untuk menghapus spasi yang telah dimasukkan pengguna di awal dan di akhir baris.
import Foundation let url = URL(string: " https:
Foundation
menawarkan metode trimmingCharacters(in:)
, yang dengannya Anda dapat menghapus spasi di awal dan akhir baris. Anda dapat memanggil metode ini kapan pun Anda perlu menjamin kebenaran input, tetapi itu tidak terlalu nyaman. Anda dapat menggunakan pembungkus properti untuk ini.
import Foundation @propertyWrapper struct Trimmed { private(set) var value: String = "" var wrappedValue: String { get { return value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } }
struct Post { @Trimmed var title: String @Trimmed var body: String } let quine = Post(title: " Swift Property Wrappers ", body: "โฆ") quine.title
Ide serupa
@Transformed
menerapkan konversi ICU ke string @Transformed
.@Rounded
/ @Truncated
rounds atau memotong nilai string.
Ubah semantik kesetaraan dan perbandingan properti
Dalam Swift, dua string sama jika mereka setara secara kanonik , yaitu mengandung karakter yang sama. Tapi misalkan kita ingin properti string sama, tidak peka huruf besar kecil, yang dikandungnya.
@CaseInsensitive
mengimplementasikan pembungkus untuk properti tipe String
atau SubString
.
import Foundation @propertyWrapper struct CaseInsensitive<Value: StringProtocol> { var wrappedValue: Value } extension CaseInsensitive: Comparable { private func compare(_ other: CaseInsensitive) -> ComparisonResult { wrappedValue.caseInsensitiveCompare(other.wrappedValue) } static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedSame } static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedAscending } static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedDescending } }
let hello: String = "hello" let HELLO: String = "HELLO" hello == HELLO
Ide serupa
@Approximate
untuk perbandingan kasar dari properti tipe Double atau Float.@Ranked
untuk properti yang nilainya berurutan (misalnya, pangkat kartu remi).
Pencatatan Akses Properti
@Versioned
akan memungkinkan Anda untuk mencegat nilai yang ditugaskan dan mengingat kapan mereka ditetapkan.
import Foundation @propertyWrapper struct Versioned<Value> { private var value: Value private(set) var timestampedValues: [(Date, Value)] = [] var wrappedValue: Value { get { value } set { defer { timestampedValues.append((Date(), value)) } value = newValue } } init(initialValue value: Value) { self.wrappedValue = value } }
Kelas ExpenseReport
memungkinkan ExpenseReport
menyimpan stempel waktu status pemrosesan dari laporan pengeluaran.
class ExpenseReport { enum State { case submitted, received, approved, denied } @Versioned var state: State = .submitted }
Tetapi contoh di atas menunjukkan batasan serius dalam implementasi pembungkus properti saat ini, yang mengikuti dari pembatasan Swift: properti tidak dapat membuang pengecualian. Jika kami ingin menambahkan batasan ke @Versioned
untuk mencegah nilai berubah menjadi .approved
setelah mengambil nilai .denied
, maka opsi terbaik adalah fatalError()
, yang tidak cocok untuk aplikasi nyata.
class ExpenseReport { @Versioned var state: State = .submitted { willSet { if newValue == .approved, $state.timestampedValues.map { $0.1 }.contains(.denied) { fatalError("") } } } } var tripExpenses = ExpenseReport() tripExpenses.state = .denied tripExpenses.state = .approved
Ide serupa
@Audited
untuk @Audited
akses properti.@UserDefault
untuk merangkum mekanisme untuk membaca dan menyimpan data di UserDefaults
.
Keterbatasan
Properti tidak dapat membuang pengecualian
Seperti yang telah disebutkan, pembungkus properti hanya dapat menggunakan beberapa metode untuk memproses nilai yang tidak valid:
- abaikan mereka;
- menghentikan aplikasi menggunakan fatalError ().
Properti yang dibungkus tidak dapat ditandai dengan atribut `typealias`
Contoh @UnitInterval
atas, yang propertinya dibatasi oleh interval (0...1)
, tidak dapat dinyatakan sebagai
typealias UnitInterval = Clamping(0...1)
Pembatasan penggunaan komposisi beberapa pembungkus properti
Menulis pembungkus properti bukanlah operasi komutatif: urutan deklarasi akan memengaruhi perilaku. Pertimbangkan contoh di mana properti siput, yang merupakan url posting blog, dinormalisasi. Dalam hal ini, hasil normalisasi akan bervariasi tergantung pada kapan spasi diganti dengan tanda hubung, sebelum atau setelah penghapusan spasi. Karenanya, saat ini, komposisi beberapa pembungkus properti tidak didukung.
@propertyWrapper struct Dasherized { private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.replacingOccurrences(of: " ", with: "-") } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { โฆ @Dasherized @Trimmed var slug: String
Namun, batasan ini dapat dielakkan dengan menggunakan pembungkus properti bersarang.
@propertyWrapper struct TrimmedAndDasherized { @Dasherized private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { โฆ @TrimmedAndDasherized var slug: String }
Pembatasan pembungkus properti lainnya
- Tidak dapat digunakan di dalam protokol.
- Contoh properti wrapper tidak dapat dideklarasikan dalam
enum
. - Properti terbungkus yang dideklarasikan di dalam kelas tidak dapat diganti oleh properti lain.
- Properti terbungkus tidak boleh
lazy
, @NSCopying
, @NSManaged
, weak
atau unowned
. - Properti terbungkus harus menjadi satu-satunya dalam definisi (mis.,
@Lazy var (x, y) = /* ... */
). - Properti terbungkus tidak dapat memiliki
getter
dan setter
ditentukan. - Jenis properti
wrappedValue
dan variabel wrappedValue
di init(wrappedValue:)
harus memiliki tingkat akses yang sama dengan tipe wrapper properti. - Tipe properti dari
projectedValue
harus memiliki tingkat akses yang sama dengan tipe pembungkus dari properti. init()
harus memiliki tingkat akses yang sama dengan tipe pembungkus properti.
Mari kita simpulkan. Pembungkus properti di Swift memberi penulis perpustakaan akses ke perilaku tingkat tinggi yang sebelumnya disediakan untuk fungsi bahasa. Potensi mereka untuk meningkatkan keterbacaan dan mengurangi kompleksitas kode sangat besar, dan kami hanya memeriksa kemampuan alat ini secara dangkal.
Apakah Anda menggunakan pembungkus properti di proyek Anda? Tulis di komentar!