Agar satu delegate dapat menangani beberapa jenis komponen SwiftUI, buat metode callback menjadi generik dan jaga agar komponen tetap independen dari tipe konkret delegate. Panduan ini membuat pemilih tile macOS sederhana yang mempertahankan tipe konten pengirim tanpa persyaratan Self rekursif.

Pilih kontrak callback sebelum memilih sintaks

Tugas pembaca cukup spesifik: buat komponen SwiftUI yang dapat digunakan kembali dengan konten yang bisa bervariasi, sementara satu delegate menerima callback pemilihan dari setiap spesialisasi konten yang didukung. Demonstrasi ini menggunakan dua tile sintetis—teks dan lingkaran—serta menampilkan pengenal tile yang dipilih.

Swift mendefinisikan delegasi sebagai cara berbasis protokol bagi kelas atau struktur untuk menyerahkan tanggung jawab kepada instans lain. Delegate dapat menerima tindakan beserta pengirimnya, seperti yang ditunjukkan dalam contoh delegasi di panduan bahasa. Protokol

Untuk komponen ini, kontrak yang dimaksud adalah: “Untuk setiap jenis konten yang mengikuti View, delegate dapat menerima tile yang berisi konten tersebut.” Tempatkan parameter tipe pada metode callback. Fungsi generik Swift memilih argumen tipe konkretnya pada setiap pemanggilan, sedangkan tipe terkait ditetapkan oleh konformansi protokol. Keduanya merupakan komitmen yang berbeda. Generik

Persyaratan Bentuk antarmuka yang sesuai Konsekuensi
Satu delegate menangani setiap jenis konten yang didukung Metode callback generik Setiap callback dapat memiliki jenis konten yang berbeda.
Setiap delegate menangani satu jenis konten tetap Tipe terkait Setiap konformansi menyediakan satu jenis konten.
Callback hanya memerlukan pengenal tile Callback yang menerima pengenal Callback tidak bergantung pada jenis untuk rendering.

Tabel ini merupakan panduan desain yang diturunkan dari aturan bahasa tersebut. Pilih baris pertama jika mempertahankan tipe pengirim merupakan bagian dari API. Pilih baris ketiga jika pengenal pengirim menyediakan semua yang dibutuhkan penerima; membawa seluruh komponen akan menambahkan dependensi yang tidak perlu.

Hapus dependensi pada tipe delegate itu sendiri

Desain yang sangat terikat dapat membuat komponen generik terhadap delegatenya, lalu merujuk ke komponen tersebut sebagai Component<Self> di dalam protokol delegate. Di sini, Self berarti tipe delegate yang nantinya mengikuti protokol. Tipe terkait juga menjadi bagian dari tipe konforman tersebut. Deklarasi

Bentuk itu membuat pemeriksaan callback bergantung pada spesialisasi komponen yang juga bergantung pada delegate. Untuk memastikan apakah deklarasi rekursif tertentu diterima, seluruh batasannya dan diagnostik compiler perlu diperiksa. Tidak tepat jika dikatakan Swift menolak setiap hubungan protokol rekursif: SE-0157 menambahkan batasan tipe terkait rekursif di Swift 4.1. Mendukung batasan rekursif pada tipe terkait

Untuk tugas yang dipilih, tipe konkret delegate sama sekali tidak perlu muncul dalam tipe komponen. Desain berikut memberi Tile satu parameter generik, Content. Delegate yang disimpan bertipe any TileDelegate, dan persyaratan protokol memperkenalkan parameter Content tersendiri pada tingkat metode. Callback mempertahankan tipe konten pengirim tanpa menyatakannya melalui Self milik delegate.

Penulisan any secara eksplisit menunjukkan nilai eksistensial: nilai yang disimpan dapat menampung instans tipe yang mengikuti protokol. SE-0335 memperkenalkan penulisan tersebut di Swift 5.6. Menggunakan eksistensial untuk delegate tidak mengharuskan konten tile dihapus tipenya menjadi AnyView. Memperkenalkan existential any

Buat pemilih tile macOS yang lengkap

Gunakan target app SwiftUI macOS dengan compiler Swift 6, seperti compiler yang disertakan dengan Xcode 16 atau versi lebih baru. Atur target deployment macOS ke 11.0 atau lebih baru untuk penyimpanan @StateObject dalam contoh ini. Apple mendokumentasikan bahwa StateObject tersedia mulai macOS 11.0. StateObject

Ganti deklarasi app yang dibuat otomatis dan tampilan awal dengan satu berkas sumber berikut. Pastikan target hanya memiliki satu deklarasi app @main; Swift hanya mengizinkan satu pendekatan titik masuk program. Deklarasi

import SwiftUI
import Combine

@MainActor
protocol TileDelegate: AnyObject {
    func didChoose<Content: View>(_ tile: Tile<Content>)
}

@MainActor
struct Tile<Content: View>: View {
    let id: String
    let delegate: any TileDelegate
    let content: Content

    init(
        id: String,
        delegate: any TileDelegate,
        @ViewBuilder content: () -> Content
    ) {
        self.id = id
        self.delegate = delegate
        self.content = content()
    }

    var body: some View {
        Button(action: {
            delegate.didChoose(self)
        }) {
            VStack(spacing: 8) {
                content
                    .frame(width: 100, height: 50)
                Text("Choose \(id)")
            }
            .padding(8)
        }
    }
}

@MainActor
final class SelectionModel: ObservableObject, TileDelegate {
    @Published private(set) var selection = "No tile selected"

    func didChoose<Content: View>(_ tile: Tile<Content>) {
        selection = "Selected: \(tile.id)"
    }
}

@MainActor
struct ChooserView: View {
    @StateObject private var model = SelectionModel()

    var body: some View {
        VStack(spacing: 16) {
            HStack(spacing: 16) {
                Tile(id: "text", delegate: model) {
                    Text("Sample")
                }

                Tile(id: "circle", delegate: model) {
                    Circle()
                }
            }

            Text(model.selection)
        }
        .padding(24)
        .frame(minWidth: 340, minHeight: 180)
    }
}

@main
@MainActor
struct DelegateDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ChooserView()
        }
    }
}

Ini adalah contoh app lengkap, termasuk impor, delegate, komponen, model, dan titik masuk. Contoh ini telah diperiksa, tetapi tidak dikompilasi atau dijalankan di sini karena lingkungan yang tersedia tidak memiliki compiler Swift atau SDK macOS.

API Button Apple menerima tindakan dan label yang menghasilkan tampilan. Dalam contoh ini, mengeklik tile memanggil delegate dari tindakan tombol; mengevaluasi body membuat antarmuka tanpa mengirimkan peristiwa pemilihan. Button

Bangun dan jalankan app di Mac. Awalnya, status akan menampilkan No tile selected. Mengeklik Choose text akan menghasilkan Selected: text; mengeklik Choose circle akan menghasilkan Selected: circle. Ini adalah hasil yang diharapkan dari contoh, bukan hasil eksekusi yang dilaporkan.

Pahami apa yang dipertahankan oleh contoh ini

Tile pertama bertipe Tile<Text>, sedangkan yang kedua bertipe Tile<Circle>. Keduanya menggunakan instans SelectionModel yang sama. Callback generiknya memenuhi persyaratan protokol untuk kedua jenis konten, sekaligus membaca properti id yang sama. Inilah manfaat praktis menempatkan parameter generik pada metode.

Penerima juga dapat mengakses tile.content sebagai Content. Namun, di dalam implementasi generik, batasan yang dideklarasikan hanya menjamin kemampuan View. Kode yang memerlukan operasi khusus teks perlu kontrak atau informasi tambahan yang berbeda; callback tidak dapat mengasumsikan bahwa setiap nilai konten adalah Text. Ini mengikuti model generik Swift yang berbasis batasan. Generik

Komponen mengimplementasikan body dengan some View, mengikuti pola tampilan kustom yang didokumentasikan Apple. Deklarasi View saat ini juga membawa isolasi main actor. Contoh ini secara eksplisit menandai protokol dan tipe yang berhadapan dengan UI sebagai @MainActor, sehingga callback dan pembaruan model berbagi isolasi tersebut. View

@StateObject menyediakan penyimpanan model yang stabil selama identitas tampilan yang menaunginya tetap hidup. Perubahan pada pilihan yang dipublikasikan model akan memperbarui tampilan status yang bergantung padanya. Memindahkan pembuatan model ke ekspresi komputasi biasa tidak lagi menyatakan kepemilikan tersebut melalui StateObject. StateObject

Contoh ini menyimpan delegate wajib dengan referensi kuat. Modelnya hanya menyimpan string pilihan, sehingga graf objek yang ditunjukkan tidak memiliki referensi kembali ke tile. Jika nantinya model menyimpan pengirim, tinjau kembali kepemilikan. Panduan bahasa Apple menunjukkan delegate yang terikat ke kelas dengan penyimpanan lemah ketika hubungan tersebut memerlukannya. Protokol

Kapan tipe terkait menjadi kontrak yang lebih baik

Misalkan penerima hanya boleh menerima tile teks. Protokol alternatif dapat mendeklarasikan associatedtype Content: View dan mensyaratkan func didChoose(_ tile: Tile<Content>). Ini menyatakan satu jenis konten untuk setiap konformansi. Menjadikan Content sebagai tipe terkait utama menyediakan sintaks batasan yang praktis; hal itu tidak mengubah persyaratan menjadi metode yang menerima setiap jenis konten. SE-0346 memperkenalkan tipe terkait utama di Swift 5.7. Persyaratan kesamaan tipe ringan untuk tipe terkait utama

Alternatif ini akan menjadi versi API yang berbeda. Anda juga perlu mengubah properti delegate komponen dan parameter inisialisatornya menjadi eksistensial terbatas yang sesuai, seperti any FixedTileDelegate<Content>, serta mengubah setiap penerima agar menyediakan tipe terkait tetap dan callback nongenerik yang bersesuaian. Konformansi khusus teks tidak lagi dapat melayani tile lingkaran melalui konformansi yang sama.

SE-0353 menambahkan tipe eksistensial terbatas di Swift 5.7, sehingga tipe terkait utama pada eksistensial dapat diikat ke tipe tertentu. Mengikat satu tipe terkait tetap menyatakan hubungan yang tetap. Itu tidak menyediakan sekumpulan implementasi callback untuk sembarang jenis konten. Tipe Eksistensial Terbatas

Pisahkan dukungan compiler dari dukungan deployment. SE-0353 secara eksplisit menyebutkan perubahan runtime yang tidak dapat di-deploy mundur ke rilis OS yang sudah ada. Sebelum menggunakan alternatif ini pada target deployment lama, periksa diagnostik ketersediaan pada target sebenarnya. Contoh lengkap di atas menggunakan eksistensial delegate biasa dan tidak memerlukan penyimpanan eksistensial terbatas. Tipe Eksistensial Terbatas

Diagnosis adaptasi yang gagal dengan urutan yang berguna

  1. Periksa cakupan callback yang dimaksud. Jika satu delegate harus menangani tile teks dan lingkaran, witness-nya harus mempertahankan signature metode generik. Metode yang hanya menerima Tile<Text> tidak memenuhi persyaratan tersebut.
  2. Periksa parameter generik komponen. Dalam desain ini, Tile bergantung pada Content; tipenya tidak mencakup tipe konkret delegate.
  3. Periksa isolasi aktor. Pertahankan callback UI dan model teramati dalam contoh ini pada main actor. Pemanggilan dari konteks isolasi lain harus mematuhi batas tersebut.
  4. Periksa lokasi peristiwa. Kirimkan pemilihan dari tindakan tombol, bukan saat menghitung body atau menginisialisasi tampilan.
  5. Periksa kepemilikan model. Pertahankan @StateObject pada tampilan yang menaungi dan hindari membuat instans model kedua untuk callback.

Jika adaptasi masih gagal, sederhanakan menjadi satu tile dan satu implementasi delegate, lalu tambahkan kembali jenis konten kedua. Urutan ini membedakan masalah witness protokol dari masalah komposisi tampilan. Dua jenis konten berbeda dalam contoh ini berguna untuk memeriksa kontrak: keduanya harus mencapai callback generik yang sama tanpa cast atau penghapusan tipe konten.

Keputusan desain yang utama adalah menentukan di mana variasi tipe ditempatkan. Untuk delegate yang menangani setiap spesialisasi komponen, tempatkan variasi tersebut pada metode callback dan jaga agar tipe komponen independen dari identitas konkret delegate. API yang dihasilkan mempertahankan pengirim bertipe, sekaligus menjaga alur pemilihan macOS tetap sederhana untuk diperiksa dan diperluas.

Sumber