Jadikan komponen SwiftUI Anda generik terhadap tipe yang mematuhi protokol tersebut, lalu deklarasikan view tersimpannya sebagai Provider.Content. Ini mempertahankan hubungan antara provider dan tipe view terkaitnya di seluruh komponen.
Tutorial ini membuat jendela macOS kecil yang berisi dua ubin sintetis. Tugasnya adalah menyimpan provider dan view yang dihasilkannya tanpa mengganti tipe terkait dengan any View atau AnyView. Pola yang sama berlaku saat komponen desktop yang dapat digunakan kembali menerima label kustom, aksesori, atau provider konten.
Hubungan apa yang harus dipertahankan oleh properti tersimpan?
Tipe terkait menamai tipe yang dipilih oleh setiap konformansi protokol. Jika sebuah protokol mendeklarasikan associatedtype Content: View, provider yang mematuhinya harus menyediakan satu tipe konten tertentu. Kode generik dapat merujuk ke tipe itu sebagai Provider.Content, bahkan saat implementasinya tersembunyi di balik hasil opak. Swift mendokumentasikan tipe terkait dan batasan generik dalam Bab Generics.
Karena itu, komponen memerlukan dua deklarasi yang saling terhubung: parameter generik yang dibatasi pada protokol provider, dan properti konten tersimpan yang tipenya adalah tipe terkait parameter tersebut. Mendeklarasikan dua properti independen yang sekadar mematuhi protokol tidak menyatakan hubungan ini.
SwiftUI sendiri menggunakan kontrak terkait: View mendeklarasikan associatedtype Body: View, dan Swift menyimpulkan tipe terkait itu dari implementasi body pada view kustom. Content milik provider Anda dan Body milik komponen adalah tipe terkait yang terpisah; keduanya tidak harus sama. Lihat dokumentasi Body dari Apple.
Buat contoh macOS yang lengkap
Gunakan proyek aplikasi macOS Xcode dengan SwiftUI dan target deployment macOS 13 atau lebih baru untuk contoh ini. Ganti deklarasi app dan content-view yang dihasilkan dengan satu file berikut, dan pastikan hanya ada satu deklarasi @main dalam target. Apple mendokumentasikan struktur titik masuk app ini dan penggunaan WindowGroup dalam App.
Contoh ini sengaja menggunakan sintaks tipe terkait dan generik bernama yang sudah lama tersedia. Hasil opak diimplementasikan pada Swift 5.1; sintaks primary-associated-type opsional yang dibahas nanti memerlukan Swift 5.7. File lengkap ini ditujukan untuk toolchain Swift 6 saat ini. Isinya telah diperiksa berdasarkan dokumentasi yang diperoleh, tetapi tidak dikompilasi di sini karena lingkungan ini tidak memiliki kompiler Swift. Target deployment macOS yang dipilih adalah prasyarat contoh, bukan klaim tentang versi OS paling awal yang mendukung setiap fitur dasarnya. Lihat SE-0244 dan SE-0346.
import SwiftUI
@MainActor
protocol TileProvider {
associatedtype Content: View
var title: String { get }
func makeContent() -> Content
}
@MainActor
struct TextTile: TileProvider {
let title: String
let message: String
func makeContent() -> Text {
Text(message)
}
}
@MainActor
struct CircleTile: TileProvider {
let title: String
let caption: String
func makeContent() -> some View {
HStack(spacing: 8) {
Circle()
.fill(Color.blue)
.frame(width: 16, height: 16)
Text(caption)
}
}
}
@MainActor
struct TileCard<Provider: TileProvider>: View {
private let provider: Provider
private let content: Provider.Content
init(provider: Provider) {
self.provider = provider
self.content = provider.makeContent()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(provider.title)
.font(.headline)
content
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.12))
}
}
@MainActor
struct ContentView: View {
var body: some View {
VStack(spacing: 12) {
TileCard(provider: TextTile(
title: "Text tile",
message: "Blue square"
))
TileCard(provider: CircleTile(
title: "Shape tile",
caption: "Blue circle"
))
}
.padding()
.frame(width: 360)
}
}
@main
@MainActor
struct AssociatedViewDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Build dan jalankan target macOS. Hasil yang diharapkan adalah jendela berisi ubin teks dan ubin dengan lingkaran biru di samping keterangannya. Keduanya menggunakan komponen generik yang sama, dengan tipe provider disimpulkan secara terpisah di setiap titik pemanggilan.
Anotasi @MainActor yang eksplisit menjaga deklarasi penghasil UI ini dalam konteks aktor yang sama. Dokumentasi View Apple saat ini mendeklarasikan protokol tersebut dengan isolasi aktor utama. Anotasi di sini juga memperjelas konteks yang dimaksud untuk protokol provider kustom.
Mengapa komponen generik mempertahankan tipe tersebut?
Untuk kartu pertama, Provider adalah TextTile, jadi Provider.Content adalah Text. Untuk kartu kedua, Provider adalah CircleTile, dan tipe konten terkaitnya adalah hasil opak dari CircleTile.makeContent(). Anda dapat menamai tipe itu melalui CircleTile.Content tanpa menuliskan tipe tata letak SwiftUI yang bersarang.
Penetapan yang penting adalah self.content = provider.makeContent(). Protokol menjamin bahwa metode ini mengembalikan tepat Provider.Content, yang sama persis dengan tipe yang dideklarasikan untuk properti tersimpan. Ini adalah hubungan pada waktu kompilasi yang diturunkan dari batasan generik, bukan perbandingan tipe saat runtime. Bab Generics Swift menjelaskan cara mengakses tipe terkait melalui parameter generik yang dibatasi.
Penggunaan some View dalam implementasi konformansi tidak menghapus hubungan ini. Hasil opak menyembunyikan implementasi konkret sambil mempertahankan identitas tipenya. Karena itu, hasil tersebut cocok dijadikan pemenuh tipe terkait. Swift menjelaskan perbedaannya dalam Tipe Protokol Opak dan Terbungkus.
Kartu-kartu ini memiliki spesialisasi yang berbeda: TileCard<TextTile> dan TileCard<CircleTile>. Menggunakan kembali deklarasi sumber tidak mengharuskan setiap kartu memiliki satu tipe konten tersimpan universal.
Mengapa properti opak atau eksistensial yang independen tidak menyelesaikannya?
Berikut adalah fragmen diagnostik, bukan pengganti program lengkap:
// Diagnostic fragment: no initializer establishes the opaque type.
let content: some View
// Diagnostic fragment: hides which conforming provider is stored.
let provider: any TileProvider
// Diagnostic fragment: stores an existential view value.
let content: any View
Properti tersimpan opak memerlukan penginisialisasi deklarasi yang menetapkan tipe dasarnya. Menulis some View tidak membuat parameter generik yang dipilih pemanggil dan dapat diisi nanti oleh penginisialisasi komponen arbitrer. Misalnya, let content: some View = Text("Sample") menetapkan tipe untuk deklarasi tersebut; itu tidak menghubungkan properti dengan tipe terkait milik properti lain. Aturan properti tersimpan ini didokumentasikan dalam SE-0244: Tipe Hasil Opak.
Tipe eksistensial seperti any TileProvider mengizinkan penyimpanan berbagai tipe yang mematuhi provider. Tipe konformansi spesifik disembunyikan, sehingga deklarasi tidak lagi menampilkan hubungan sebagai Provider.Content. Demikian pula, any View adalah wadah eksistensial; kontraknya berbeda dari tipe generik bernama yang mematuhi View. Lihat penjelasan Swift tentang tipe protokol terbungkus.
Swift dapat membuka argumen eksistensial secara implisit untuk beberapa pemanggilan generik. Namun, itu tidak membuat setiap eksistensial dapat digunakan dalam setiap penginisialisasi generik. Secara khusus, SE-0352 membatasi pembukaan saat tipe dasar yang tidak diketahui akan keluar melalui hasil generik seperti X<T>. Spesialisasi komponen generik dapat menemui batasan ini. Pertahankan provider tetap generik sebelum membuat kartu, alih-alih menganggap eksistensial selalu dapat dikonversi kembali menjadi spesialisasi yang persisten. Lihat SE-0352: Eksistensial yang Dibuka Secara Implisit.
Pilih penyimpanan berdasarkan kapan konten harus dibuat
Contoh ini memanggil makeContent() saat setiap TileCard diinisialisasi dan menyimpan nilai view yang dikembalikan. Contoh ini tidak membentuk cache persisten untuk inisialisasi komponen berikutnya. Mempertahankan kedua properti menunjukkan hubungan tipe yang diminta, tetapi komponen yang hanya memerlukan view hasilnya dapat menghilangkan provider tersimpan.
Jika komponen harus meminta konten setiap kali body dievaluasi, simpan provider saja. Berikut adalah deklarasi komponen pengganti yang dapat digunakan bersama protokol dan app di atas:
@MainActor
struct TileCard<Provider: TileProvider>: View {
private let provider: Provider
init(provider: Provider) {
self.provider = provider
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(provider.title)
.font(.headline)
provider.makeContent()
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.12))
}
}
Kedua desain mempertahankan tipe terkait. Perbedaannya langsung mengikuti lokasi pemanggilan metode: inisialisasi pada desain pertama, evaluasi body pada desain kedua. Tentukan waktu tersebut dengan sengaja, dan pastikan pembuatan konten tidak melakukan pekerjaan yang bergantung pada pemanggilan tepat satu kali.
Gunakan panduan keputusan ini saat menyesuaikan contoh:
| Kebutuhan komponen | Strategi deklarasi | Hubungan tipe |
|---|---|---|
| Menyimpan provider dan view yang dihasilkannya | Provider generik ditambah Provider.Content |
Mempertahankan identitas provider dan tipe kontennya |
| Membuat konten saat body dievaluasi | Simpan provider generik | Mempertahankan tipe terkait tanpa menyimpan nilai konten terpisah |
| Menyimpan provider dengan tipe konten umum yang diketahui | Pertimbangkan eksistensial berbatasan | Mempertahankan kesamaan konten yang ditentukan sambil menyembunyikan identitas provider |
| Sengaja menghapus tipe implementasi view yang berbeda | Gunakan AnyView pada batas yang dipilih |
Menyimpan tipe pembungkus alih-alih menampilkan tipe view asli |
Apakah tipe terkait utama menghilangkan kebutuhan akan generik?
Tipe terkait utama menawarkan cara yang lebih singkat untuk menyatakan batasan kesamaan tipe tertentu. Jika Anda memiliki protokol tersebut, header alternatif ini menjadikan Content sebagai tipe terkait utama; persyaratan protokol lainnya tetap sama:
// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
associatedtype Content: View
var title: String { get }
func makeContent() -> Content
}
Dengan deklarasi itu, any TileProvider<Text> menjelaskan eksistensial yang tipe konten terkaitnya diketahui sebagai Text. Implementasi provider yang berbeda dapat berbagi tipe penyimpanan itu hanya jika konformansinya menampilkan kesamaan konten yang persis sama. Sintaks ini membatasi tipe terkait; protokol tidak diubah menjadi struct generik. Lihat SE-0346 dan SE-0353.
Ini tidak menggabungkan tipe view tak dikenal yang sembarang menjadi satu tipe konkret yang dipertahankan. Secara khusus, CircleTile menampilkan hasil konten opak, sehingga pemanggil tidak bisa begitu saja memperlakukan hasil itu sebagai Text. Gunakan komponen generik semula ketika konten terkait provider harus tetap tidak dibatasi.
Eksistensial berbatasan diimplementasikan pada Swift 5.7, dan SE-0353 secara eksplisit menyebutkan batasan runtime dan deployment ke versi sebelumnya. Karena itu, dukungan kompiler saja tidak cukup untuk memastikan kompatibilitas deployment bagi desain opsional tersebut. Periksa diagnostik ketersediaan toolchain target sebelum mengadopsinya. Contoh utama tidak bergantung pada penyimpanan eksistensial berbatasan. Lihat pembahasan ABI SE-0353.
Apa yang harus diperiksa saat kompilasi gagal?
Telusuri tipe dari persyaratan protokol ke properti tersimpan sebelum mengubah tata letak:
- Pastikan protokol mendeklarasikan tipe terkait yang dibatasi ke
View. - Pastikan komponen memiliki parameter provider generik bernama yang dibatasi ke protokol tersebut.
- Deklarasikan konten tersimpan sebagai
Provider.Content, sesuai dengan tipe kembalian metode. - Periksa apakah parent sudah mengubah provider menjadi
any TileProvider. - Jika menambahkan modifier sebelum penyimpanan, periksa tipe hasilnya: view yang dimodifikasi mungkin bukan lagi
Provider.Content. - Periksa diagnostik isolasi aktor secara terpisah dari diagnostik tipe terkait.
Apple mendokumentasikan bahwa modifier view membungkus sebuah view di dalam view lain. Akibatnya, menetapkan provider.makeContent().padding() ke properti yang dideklarasikan sebagai Provider.Content mengubah kontrak yang harus dipenuhi oleh penetapan tersebut. Simpan konten asli dan terapkan modifier presentasi di body, atau rancang ulang tipe tersimpan agar menjelaskan hasil yang dimodifikasi. Lihat View.
Builder juga memiliki batas versi. Dokumentasi Apple saat ini menjelaskan perilaku ViewBuilder untuk Xcode 26 dan versi sebelumnya, lalu perilaku pembuatan konten yang berbeda mulai Xcode 27. Contoh ini menghindari persyaratan builder kustom: setiap provider mengembalikan satu ekspresi eksplisit. Jika Anda mengembangkannya dengan API provider berbasis builder, ikuti dokumentasi untuk versi Xcode yang benar-benar Anda gunakan untuk build. Lihat ViewBuilder.
Jika Anda sengaja memilih AnyView, perhitungkan perilaku yang didokumentasikan: perubahan tipe view yang dibungkus akan menghancurkan hierarki lama dan membuat yang baru. Ini adalah keputusan desain terpisah dari mempertahankan tipe terkait dalam penyimpanan. Apple mendokumentasikannya dalam AnyView.
Untuk komponen ini, pertahankan hubungan tersebut agar terlihat pada batas penyimpanannya: Provider generik, hasil bertipe Provider.Content, dan properti tersimpan dengan tipe yang sama. Dengan begitu, Anda dapat mengubah tata letak internal provider sementara kompiler meneruskan tipe view terkaitnya melalui komponen.
Sumber
- Generics — Swift.org
- Tipe Protokol Opak dan Terbungkus — Swift.org
- View — Apple
- Body — Apple
- App — Apple
- ViewBuilder — Apple
- AnyView — Apple
- SE-0244: Tipe Hasil Opak — Swift Evolution
- SE-0346: Persyaratan kesamaan tipe ringan untuk tipe terkait utama — Swift Evolution
- SE-0352: Eksistensial yang Dibuka Secara Implisit — Swift Evolution
- SE-0353: Tipe Eksistensial Berbatasan — Swift Evolution