Untuk menyimpan berbagai shape SwiftUI dalam satu set sambil menerapkan antarmuka bersama, gunakan pembungkus Hashable konkret dengan initializer yang dibatasi protokol. Tutorial ini membuat palet shape Mac kecil dengan aturan duplikat yang eksplisit dan operasi shape yang dapat dipanggil.
Tugasnya spesifik: kumpulkan komponen disk dan tile sintetis, tolak nilai yang tidak terkait pada waktu kompilasi, hapus duplikat, dan tampilkan komponen yang tersisa di jendela SwiftUI. Koleksi ini menerima beberapa tipe komponen melalui protokol bersama yang memperluas Shape dari SwiftUI.
Mengapa set berisi nilai protokol tidak memenuhi kontrak ini?
Set di Swift mensyaratkan tipe elemennya mematuhi Hashable. Menyatakan bahwa setiap nilai di dalam eksistensial mematuhi Hashable tidak membuat tipe eksistensial itu sendiri mematuhinya. Akibatnya, Set<any PaletteShape> tidak otomatis valid hanya karena PaletteShape mewarisi Hashable. Apple mendokumentasikan batasan elemen koleksi, sedangkan Swift Evolution menjelaskan perbedaan antara wadah eksistensial dan nilai konkret yang mendasarinya dan mematuhi protokol. Set, Implicitly Opened Existentials.
Eksistensial seperti any PaletteShape dapat menampung tipe berbeda yang mematuhi protokol. Kesulitannya terletak pada kesetaraan: persyaratan Equatable yang diwarisi membandingkan dua nilai dengan tipe Self yang sama, sedangkan dua wadah eksistensial mungkin menampung tipe konkret yang berbeda. Dukungan Swift yang diperluas untuk nilai eksistensial tidak menyediakan operasi kesetaraan universal bagi wadah tersebut. Unlock existentials for all protocols.
Alternatif berikut menyelesaikan bagian masalah yang berbeda:
| Representasi | Tipe konkret campuran? | Antarmuka bersama diterapkan? | Cocok sebagai elemen set? |
|---|---|---|---|
[any PaletteShape] |
Ya | Ya | Array tidak menjamin keunikan dalam set |
Set<S> dengan S: PaletteShape |
Hanya satu S yang dipilih |
Ya | Ya, untuk satu tipe konkret tersebut |
Set<AnyHashable> |
Ya | Tidak ada batasan shape | Ya |
Set<AnyPaletteShape> di bawah |
Ya, di dalam pembungkus | Ya, saat konstruksi | Ya |
AnyHashable menyediakan kesetaraan dan hashing yang tipe konkretnya dihapus, tetapi initializer-nya menerima nilai hashable secara umum. Karena itu, set yang hanya berisi tipe ini juga dapat menerima string atau integer. Pembungkus kustom di bawah membatasi elemen yang masuk agar hanya berasal dari protokol shape aplikasi, sekaligus mempertahankan antarmuka shape bersama nilai hashable yang tipe konkretnya dihapus. AnyHashable.
Apa yang dianggap sebagai shape duplikat?
Tentukan kesetaraan sebelum menerapkan pembungkus. Untuk palet ini, dua komponen dianggap duplikat jika memiliki tipe komponen konkret yang sama dan nilai tersimpan yang setara. Disk berlabel Sample dan tile berlabel Sample tetap menjadi entri terpisah. Dua disk dengan label yang sama menjadi satu.
Label adalah satu-satunya properti tersimpan pada setiap komponen contoh, jadi kesetaraan dan hashing yang disintesis mencakupnya. Ini adalah pilihan model untuk ilustrasi: kemiripan visual saja tidak mendefinisikan kesetaraan. Apple mendokumentasikan sintesis Hashable otomatis untuk struktur yang properti tersimpannya hashable, dan mensyaratkan nilai yang setara memasukkan komponen yang sama ke dalam hasher-nya. Hashable.
Pembungkus ini juga menyertakan identifier tipe dalam kesetaraan dan hashing. Ini membuat aturan tipe yang sama menjadi eksplisit, alih-alih bergantung pada asumsi tentang perbandingan nilai yang tipe konkretnya dihapus. AnyHashable dapat membandingkan nilai tertentu dari tipe dasar yang berbeda menggunakan representasi kanonis; Apple tidak mendefinisikannya sebagai pembeda tipe konkret tanpa syarat. ObjectIdentifier mendukung metatipe, sehingga contoh ini dapat mengidentifikasi tipe masukan generik. AnyHashable, ObjectIdentifier.
Bangun contoh palet Mac lengkap
Gunakan proyek aplikasi SwiftUI macOS di Xcode dengan compiler Swift 5.7 atau yang lebih baru. Untuk baseline yang dapat direproduksi, pilih mode bahasa Swift 5 dan target deployment macOS 13.0 atau yang lebih baru. Target deployment ini adalah pilihan konservatif untuk tutorial ini, bukan klaim bahwa setiap API di bawah memerlukan macOS 13.
Batas versi compiler penting karena contoh ini menyimpan any PaletteShape, yang protokol turunannya mencakup persyaratan associated type dan Self. SE-0309 mencatat dukungan eksistensial yang diperluas ini sebagai fitur yang diterapkan di Swift 5.7. Contoh ini tidak memerlukan fitur konformansi eksistensial tergeneralisasi yang lebih baru. Unlock existentials for all protocols.
Ganti deklarasi app dan tampilan konten yang dibuat otomatis dengan berkas sumber lengkap berikut. Pertahankan hanya satu deklarasi @main di target app. Contoh ini mencakup definisi komponen, pembungkus, nilai contoh, pemeriksaan, tampilan, dan titik masuk app. Swift tidak tersedia di lingkungan riset, jadi kode ini telah diperiksa tetapi tidak dikompilasi atau dijalankan di sini.
import SwiftUI
protocol PaletteShape: Shape, Hashable {
var label: String { get }
}
struct Disk: PaletteShape {
let label: String
func path(in rect: CGRect) -> Path {
Path(ellipseIn: rect)
}
}
struct Tile: PaletteShape {
let label: String
func path(in rect: CGRect) -> Path {
var result = Path()
result.addRect(rect)
return result
}
}
struct AnyPaletteShape: Hashable {
private let base: any PaletteShape
private let typeID: ObjectIdentifier
private let key: AnyHashable
init<S: PaletteShape>(_ value: S) {
base = value
typeID = ObjectIdentifier(S.self)
key = AnyHashable(value)
}
var label: String { base.label }
func path(in rect: CGRect) -> Path {
base.path(in: rect)
}
static func == (
lhs: AnyPaletteShape,
rhs: AnyPaletteShape
) -> Bool {
lhs.typeID == rhs.typeID && lhs.key == rhs.key
}
func hash(into hasher: inout Hasher) {
hasher.combine(typeID)
hasher.combine(key)
}
}
func makePalette() -> Set<AnyPaletteShape> {
var result: Set<AnyPaletteShape> = [
AnyPaletteShape(Disk(label: "Disk")),
AnyPaletteShape(Tile(label: "Tile"))
]
let duplicate = result.insert(
AnyPaletteShape(Disk(label: "Disk"))
)
precondition(!duplicate.inserted)
precondition(result.count == 2)
precondition(result.contains(
AnyPaletteShape(Tile(label: "Tile"))
))
let sameLabel: Set<AnyPaletteShape> = [
AnyPaletteShape(Disk(label: "Sample")),
AnyPaletteShape(Tile(label: "Sample"))
]
precondition(sameLabel.count == 2)
return result
}
struct ContentView: View {
private let shapes = makePalette()
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Shape Palette")
.font(.title)
ForEach(
shapes.sorted { $0.label < $1.label },
id: \.self
) { item in
HStack(spacing: 16) {
GeometryReader { geometry in
item.path(in: CGRect(
origin: .zero,
size: geometry.size
))
.fill(Color.blue)
}
.frame(width: 56, height: 56)
Text(item.label)
}
}
}
.padding(24)
.frame(minWidth: 300, minHeight: 220)
}
}
@main
struct ShapePaletteApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
PaletteShape mewarisi protokol Shape SwiftUI yang sebenarnya. Disk dan tile menyediakan path(in:), persyaratan menggambar yang didokumentasikan oleh Apple. Implementasinya menggunakan operasi Path yang terdokumentasi untuk membuat elips dan persegi panjang. Bingkai tampilan persegi membuat elips contoh tampak melingkar. Shape, Path.
Initializer yang dibatasi adalah aturan masuknya: init<S: PaletteShape> hanya menerima tipe yang mematuhi protokol bersama. Properti tersimpan bersifat privat, sehingga pemanggil tidak dapat mengganti shape yang dipertahankan atau kunci kesetaraannya secara terpisah. AnyPaletteShape("Disk") seharusnya menghasilkan kesalahan konformansi pada waktu kompilasi karena String tidak mematuhi PaletteShape; jangan sertakan penolakan yang disengaja ini dalam berkas yang dapat dijalankan.
Meskipun set ini memiliki satu tipe elemen konkret, setiap pembungkus dapat mempertahankan shape konkret yang berbeda. Anggota label dan path(in:) meneruskan operasi yang diperlukan palet ini. Pembungkus itu sendiri mendeklarasikan Hashable; pembungkus itu tidak mendeklarasikan Shape.
Bagaimana memverifikasi penyisipan, keanggotaan, dan urutan tampilan?
Jalankan app dalam build debug. Pemeriksaan yang disertakan menyatakan tiga hasil yang diharapkan: penyisipan disk kedua yang setara menghasilkan inserted bernilai false, pemeriksaan keanggotaan menemukan tile yang setara, dan komponen dengan tipe berbeda tetap ada meskipun labelnya sama. Apple mendokumentasikan bahwa insert(_:) mengembalikan flag penyisipan sekaligus anggota hasilnya. Set.
Jendela yang diharapkan berisi dua baris, Disk lalu Tile, dengan grafik berwarna biru. Pengurutan adalah operasi tampilan terpisah karena set tidak memiliki urutan elemen yang ditetapkan. Array hasilnya juga memenuhi persyaratan koleksi random-access milik ForEach; id: \.self menggunakan nilai hashable pembungkus sebagai identifier baris. Set, ForEach.
Urutan ini memadai untuk nilai yang diberikan karena labelnya berbeda. Jika komponen mendatang menggunakan label yang sama, tambahkan kolom pengurutan sekunder yang deterministik. Jika baris dapat diedit dan identitasnya harus dipertahankan saat label berubah, perkenalkan identifier stabil dan tentukan secara terpisah apakah identifier tersebut menjadi bagian dari kesetaraan set.
Jangan berharap penyisipan ke dalam set mengganti payload komponen setara yang sudah ada. Jika operasi yang diinginkan adalah penggantian, Apple menyediakan update(with:). Perbedaan ini penting jika model kemudian mengecualikan properti presentasi dari kesetaraan: menyisipkan nilai setara dapat mempertahankan anggota yang sudah ada alih-alih memperbarui tampilannya. Set.
Apa batasan pembungkus ini?
Penyimpanan Hashable tidak mempertahankan semua kemampuan SwiftUI
Pembungkus ini meneruskan pembuatan path untuk palet statis. Pembungkus ini tidak meneruskan data animasi shape yang mendasarinya, operasi inset, atau setiap anggota yang diwarisi dari View. Karena itu, menggambar path yang dikembalikan tidak berarti bahwa penghapus tipe shape ini mempertahankan animasi. Shape SwiftUI juga mewarisi Animatable dan View; menerapkan satu metode gambar yang diteruskan tidak menciptakan ulang seluruh kontrak tersebut. Shape.
Jika API lain memerlukan shape dengan tipe konkret yang dihapus, SwiftUI menyediakan AnyShape, tersedia di macOS 13 dan yang lebih baru. Peran yang didokumentasikan adalah menghapus tipe konkret shape, dan daftar konformansinya tidak mencakup Hashable. Tipe ini dapat menangani batas rendering, tetapi tidak menyediakan kebijakan kesetaraan atau elemen set yang dibatasi protokol seperti yang dibuat di sini. AnyShape.
Dukungan compiler baru tidak menghapus batasan elemen set
SE-0352, yang diterapkan di Swift 5.7, memungkinkan tipe dasar eksistensial dibuka untuk pemanggilan generik yang sesuai. Teks saat ini juga mencatat perilaku mode bahasa Swift 6. Membuka nilai untuk suatu pemanggilan tidak membuat any PaletteShape menjadi tipe Hashable yang cocok untuk parameter elemen set. Tetap gunakan pembungkus konkret saat memakai desain ini pada generasi compiler tersebut. Implicitly Opened Existentials.
Memperluas model memerlukan keputusan tentang kesetaraan
Contoh ini menggunakan struktur yang tidak dapat diubah. Saat menambahkan dimensi atau konfigurasi tersimpan lainnya, tentukan apakah perubahan pada nilai tersebut membuat anggota set yang berbeda. Hashing yang disintesis akan mencakup properti tersimpan tambahan yang hashable. Untuk kesetaraan kustom, hash komponen esensial yang sama persis dalam urutan yang sama; nilai hash yang cocok saja tidak pernah membuktikan kesetaraan. Hashable.
Jika aplikasi memiliki kumpulan varian komponen yang tetap secara permanen, enum dengan nilai terkait yang hashable juga merupakan desain yang wajar: enum memusatkan kasus-kasus yang didukung. Pembungkus ini berguna ketika tipe komponen lain yang mematuhi protokol perlu dimasukkan melalui initializer yang sama tanpa memperluas daftar kasus pusat. Pilihan itu bergantung pada kebutuhan ekstensi aplikasi.
Untuk palet Mac ini, implementasinya menyediakan tempat konkret bagi setiap persyaratan: protokol mendefinisikan shape yang diterima, initializer menerapkan aturan masuk, kesetaraan mendefinisikan duplikat, dan penerusan path menyediakan rendering. Tambahkan komponen baru yang tidak dapat diubah dan mematuhi protokol dengan membungkusnya saat penyisipan, lalu perluas pemeriksaan setiap kali definisi duplikat berubah.
Sumber
- Set — Apple
- AnyHashable — Apple
- Hashable — Apple
- ObjectIdentifier — Apple
- Shape — Apple
- Path — Apple
- ForEach — Apple
- AnyShape — Apple
- Unlock existentials for all protocols — Swift Evolution
- Implicitly Opened Existentials — Swift Evolution