Untuk membuat komponen SwiftUI generik, wajibkan inisialisatornya dalam protokol atau masukkan closure yang mengembalikannya. Tutorial ini menggunakan kedua pendekatan untuk menampilkan item sintetis yang sama di jendela macOS, dengan batas compiler dan SDK yang dinyatakan secara eksplisit.
Apa yang harus dijanjikan oleh komponen generik?
Misalkan aplikasi macOS menampilkan kartu item kecil. Anda ingin panel yang dapat digunakan kembali untuk membuat kartu dari suatu item, sekaligus memungkinkan pemanggil memilih presentasinya. Pertanyaan utamanya adalah siapa yang bertanggung jawab atas pembuatan: antarmuka yang dideklarasikan oleh komponen atau pemanggil yang menyediakan komponen.
Batasan generik memberi implementasi akses ke kemampuan yang dinyatakan oleh batasan tersebut. Menulis Component: View menetapkan bahwa komponen adalah view SwiftUI; itu tidak menetapkan bahwa komponen memiliki inisialisator bernama init(item:). Bab Generics dari Swift menjelaskan batasan protokol, sedangkan dokumentasi View Apple menjelaskan properti body yang wajib dimiliki view.
Karena itu, fragmen ini memang sengaja tidak valid:
// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
Component(item: item)
}
Menambahkan inisialisator tersebut ke satu komponen konkret tidak membuatnya tersedia melalui setiap parameter generik yang hanya dibatasi ke View. API yang dapat digunakan kembali memerlukan kontrak pembuatan yang eksplisit. Gunakan tabel keputusan ini sebelum mengubah komponen:
| Persyaratan pembuatan | Antarmuka yang sesuai | Alasan |
|---|---|---|
| Setiap komponen yang berpartisipasi menerima item yang sama | Protokol yang mewajibkan init(item:) |
Pembuatan merupakan bagian dari antarmuka keluarga komponen. |
| Pemanggil memilih konfigurasi atau modifier tambahan | Closure factory yang mengembalikan komponen generik | Pembuatan tetap berada di sisi pemanggil. |
| Pemanggil sudah memiliki komponen yang selesai dibuat | Simpan nilai komponen generik | Pembuatan sudah dilakukan. |
| Presentasi berganti di antara sejumlah layout yang terbatas | Rangkai alternatif di dalam view SwiftUI | API di sekitarnya dapat mempertahankan struktur view yang konkret. |
Tabel ini merupakan panduan desain yang diturunkan dari aturan bahasa yang terdokumentasi. Dua opsi pertama diterapkan bersama di bawah ini agar perilakunya dapat dibandingkan secara langsung.
Buat contoh macOS yang lengkap
Gunakan target aplikasi macOS Xcode dengan SwiftUI dan target deployment macOS 11 atau yang lebih baru. Contoh ini ditulis untuk compiler Swift 6 dengan Apple SDK yang sesuai. Anotasi @MainActor yang dinyatakan secara eksplisit memerlukan Swift 5.5 atau yang lebih baru, jadi kode ini tidak ditujukan untuk compiler yang lebih lama. Apple mendokumentasikan bahwa WindowGroup tersedia mulai macOS 11 dan berfungsi sebagai wadah untuk hierarki view aplikasi. Lihat WindowGroup.
Ganti file titik masuk aplikasi yang dihasilkan dengan program lengkap berikut. Pastikan target hanya memiliki satu deklarasi aplikasi @main. File ContentView yang dihasilkan boleh tetap ada tanpa digunakan. Anda tidak memerlukan paket, aset eksternal, atau data contoh yang diunduh.
import SwiftUI
struct Item {
let title: String
let detail: String
}
@MainActor
protocol ItemComponent: View {
init(item: Item)
}
@MainActor
struct ItemCard: ItemComponent {
let item: Item
init(item: Item) {
self.item = item
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(item.title)
.font(.headline)
Text(item.detail)
}
.padding()
.background(Color.gray.opacity(0.12))
.cornerRadius(8)
}
}
@MainActor
struct ContractPanel<Component: ItemComponent>: View {
let item: Item
init(item: Item, component: Component.Type) {
self.item = item
}
var body: some View {
Component(item: item)
}
}
@MainActor
struct FactoryPanel<Component: View>: View {
private let makeComponent: @MainActor () -> Component
init(make: @escaping @MainActor () -> Component) {
self.makeComponent = make
}
var body: some View {
makeComponent()
}
}
@MainActor
struct DemoView: View {
private let item = Item(
title: "Sample tile",
detail: "Blue circle on a neutral background"
)
var body: some View {
VStack(alignment: .leading, spacing: 18) {
Text("Initializer contract")
ContractPanel(item: item, component: ItemCard.self)
Text("Injected factory")
FactoryPanel {
ItemCard(item: item)
.padding(.leading, 12)
}
Text("Alternative factory output")
FactoryPanel {
Text(item.title + ": " + item.detail)
}
}
.padding(24)
.frame(width: 420)
}
}
@main
@MainActor
struct ComponentDemoApp: App {
var body: some Scene {
WindowGroup {
DemoView()
}
}
}
Jalankan target aplikasi macOS. Hasil yang diharapkan adalah satu jendela berisi dua presentasi kartu dan satu presentasi teks biasa. Kartu kedua memiliki padding tambahan di sisi leading yang diberikan oleh pemanggilnya. Ini adalah contoh sintetis perilaku API, bukan hasil pengukuran. Tidak ada compiler Swift yang terpasang di lingkungan riset, jadi kode ini telah diperiksa, tetapi tidak dikompilasi atau dijalankan di sini.
Anotasi aktor utama memperjelas batas pembuatan UI. Deklarasi View Apple saat ini diberi anotasi @MainActor dan @preconcurrency; dokumentasinya juga menjelaskan bagaimana isolasi diwariskan ketika konformansi dicantumkan dalam deklarasi asli sebuah tipe. Lihat View.
Bagaimana kontrak inisialisator bekerja?
ItemComponent memperluas View dan menambahkan tepat satu persyaratan pembuatan: init(item:). Swift mendukung persyaratan inisialisator secara langsung di dalam protokol. Tipe yang mematuhi protokol menyediakan implementasinya; protokol menyatakan signature tanpa isi inisialisator. Lihat Protokol: Persyaratan Inisialisator.
Persyaratan itu membuat Component(item: item) valid di dalam ContractPanel. Argumen component: Component.Type menyediakan tipe konkret untuk inferensi generik: dengan meneruskan ItemCard.self, tipe komponen panel ini ditetapkan sebagai ItemCard. Panel tidak perlu menyimpan metatipe karena body membuat komponen melalui parameter tipe generik.
Kontrak ini berguna ketika setiap komponen harus dapat dibuat dari masukan yang sama. Misalnya, keluarga presentasi item ringkas dan diperluas milik aplikasi dapat berbagi persyaratan ini. Pemanggil kemudian dapat mengganti tipe komponen yang dipilih tanpa menulis ulang ekspresi pembuatan panel.
Komprominya terlihat ketika implementasi memerlukan konfigurasi tambahan. Mewajibkan init(item:) berarti setiap komponen yang mematuhi protokol harus menyediakan jalur pembuatan tersebut, meskipun inisialisator yang lebih disukainya juga menerima caption atau pengaturan layout. Jika informasi tambahan itu menjadi tanggung jawab pemanggil, pendekatan factory di bawah ini biasanya menyatakan persyaratannya dengan lebih langsung.
Untuk implementasi berbasis class, inisialisator yang memenuhi persyaratan protokol harus berupa required, kecuali class tersebut adalah final. Subclass yang juga menimpa inisialisator designated superclass yang cocok mungkin memerlukan required dan override. Struct seperti ItemCard tidak menggunakan required. Perbedaan ini didokumentasikan dalam Protokol.
Kapan pembuatan sebaiknya dipindahkan ke factory?
FactoryPanel menerima fungsi bertipe @MainActor () -> Component. Jaminannya hanya bahwa pemanggilan fungsi tersebut menghasilkan tipe komponen yang dipilih. Closure dapat memanggil inisialisator, menangkap konfigurasi, atau mengembalikan view yang telah diberi modifier.
Program ini menunjukkan perbedaan praktisnya: factory dapat mengembalikan ItemCard dengan padding, atau mengembalikan Text, tanpa mengharuskan tipe hasil mana pun mematuhi ItemComponent. Apple menjelaskan bahwa modifier view dapat membungkus masukannya dalam view lain, sehingga membiarkan compiler menyimpulkan tipe hasil factory berguna di sini. Lihat View.
Inisialisator menandai closure sebagai @escaping karena panel menyimpannya untuk dipanggil nanti. Bab Closures dari Swift mendokumentasikan closure escaping dan kemampuannya menangkap nilai di sekitarnya. Dalam contoh ini, closure menangkap item sintetis yang digunakan oleh view di sekitarnya.
Pilih masukan factory dengan cermat. Factory tanpa argumen berarti pemanggil menangkap semua yang diperlukan untuk pembuatan. Jika panel sendiri memilih item, factory berparameter dapat menyatakan tanggung jawab tersebut dengan lebih jelas. Berikut adalah definisi komponen pengganti yang digunakan bersama Item yang sudah ada; ini bukan aplikasi lengkap kedua:
@MainActor
struct ItemFactoryPanel<Component: View>: View {
let item: Item
private let makeComponent: @MainActor (Item) -> Component
init(
item: Item,
make: @escaping @MainActor (Item) -> Component
) {
self.item = item
self.makeComponent = make
}
var body: some View {
makeComponent(item)
}
}
Dengan definisi tersebut, panel menyediakan item dan pemanggil menyediakan presentasinya. Batasi pekerjaan factory pada pembuatan nilai view: kontrak pembuatan dalam contoh ini tidak menjanjikan eksekusi satu kali, penyimpanan persisten, atau caching.
Apakah factory memerlukan result builder?
Factory dalam program lengkap ini adalah closure biasa. Masing-masing mengembalikan satu ekspresi, jadi tidak diperlukan anotasi builder khusus. Jika diperlukan beberapa child view, mengembalikan satu VStack sudah cukup untuk API ini; stack tersebut memuat child view.
Result builder berguna ketika parameter factory itu sendiri seharusnya menerima rangkaian ekspresi deklaratif atau konten kondisional. Swift Evolution mencatat bahwa fitur result builder yang distandardisasi telah diimplementasikan di Swift 5.4. Lihat SE-0289: Result builder.
Pertahankan batas SDK saat menggunakan sintaks builder. Dokumentasi Apple saat ini menjelaskan @ViewBuilder untuk parameter penghasil view di Xcode 26 dan versi sebelumnya. Untuk Xcode 27 dan versi lebih baru, dokumentasi tersebut menjelaskan pembuatan yang tidak bergantung pada tipe dan merekomendasikan alias ContentBuilder untuk menunjukkan perilaku tersebut. Lihat ViewBuilder dan ContentBuilder. Closure biasa yang digunakan di sini tidak memerlukan alias yang lebih baru itu.
Mengapa tipe opaque atau existential tidak menggantikan kontrak ini?
some View menyembunyikan tipe hasil dasar tertentu sambil mempertahankan identitasnya. Tipe ini cocok untuk properti body dalam program ini. Tipe ini tidak menambahkan init(item:) ke antarmuka View. Nilai yang dibungkus dan ditulis sebagai any View menyembunyikan tipe dasarnya dengan cara berbeda dan tidak mempertahankan identitas tipe statis yang sama. Lihat Tipe Protokol Opaque dan Terbungkus.
Jangan mengubah ini menjadi klaim umum bahwa nilai existential tidak pernah dapat diteruskan ke fungsi generik. SE-0352 memperkenalkan existential yang dibuka secara implisit di Swift 5.7, sehingga pemanggilan yang memenuhi syarat dapat mengikat parameter generik ke tipe dasar existential. SE tersebut juga mendokumentasikan batasan, termasuk hasil yang menyematkan tipe yang dibuka dalam pembungkus generik ketika hasilnya tidak dapat direpresentasikan setelah penghapusan tipe. Lihat Existential yang Dibuka secara Implisit.
Untuk kebutuhan panel ini, mempertahankan tipe komponen konkret melalui parameter generik sudah cukup. Anda tidak perlu memperkenalkan penyimpanan existential hanya agar pemanggil dapat memilih cara membuat view.
Bagaimana cara mendiagnosis kesalahan pembuatan?
Ikuti ekspresi yang gagal dan periksa kontrak yang tersedia di lokasi tersebut:
- Jika
Component(item:)gagal, periksa apakah batasan generik mencakup protokol yang mewajibkan signature inisialisator yang persis sama. - Jika compiler tidak dapat menyimpulkan
Component, berikan metatipe konkret sepertiItemCard.self, atau factory dengan ekspresi hasil yang konkret. - Jika komponen memerlukan konfigurasi khusus dari pemanggil, pindahkan pembuatan ke factory alih-alih menambahkan persyaratan yang tidak terkait ke protokol.
- Jika menyimpan closure memicu diagnostik closure escaping, periksa anotasi
@escapingpada inisialisator. - Jika pembuatan memicu diagnostik isolasi aktor, periksa apakah lokasi pemanggilan dan tipe fungsi factory sama-sama menyatakan batas aktor utama yang dimaksud.
- Jika existential menyebabkan kesalahan hasil generik, periksa batasan tipe hasil di SE-0352 sebelum berasumsi bahwa peningkatan versi compiler menghapus batasan tersebut.
Untuk keluarga komponen macOS bersama, wajibkan inisialisator ketika pembuatan seragam merupakan bagian dari antarmuka. Gunakan factory yang diinjeksi ketika pemanggil seharusnya menyusun dan mengonfigurasi presentasi. Kedua jalur ini membuat jaminan pembuatan yang hilang menjadi eksplisit sekaligus mempertahankan tipe komponen konkret yang dibutuhkan panel.
Sumber
- Protokol — The Swift Programming Language, Swift.org.
- Generics — The Swift Programming Language, Swift.org.
- Closures — The Swift Programming Language, Swift.org.
- Tipe Protokol Opaque dan Terbungkus — The Swift Programming Language, Swift.org.
- View — Dokumentasi Developer Apple.
- WindowGroup — Dokumentasi Developer Apple.
- ViewBuilder — Dokumentasi Developer Apple.
- ContentBuilder — Dokumentasi Developer Apple.
- Result builder — Swift Evolution.
- Existential yang Dibuka secara Implisit — Swift Evolution.