Deklarasikan konten yang disediakan pemanggil sebagai (Input) -> Content, dengan Content dibatasi ke View. Ini memungkinkan komponen macOS menyediakan nilai bertipe sementara pemanggil memilih penyajiannya. Kontraknya mewajibkan penerimaan input tersebut; kontrak ini tidak dapat mewajibkan penggunaannya.

Tutorial ini membuat pratinjau swatch desktop sederhana. Komponen yang dapat digunakan kembali memiliki kontrol ukuran dan menyediakan konfigurasi sintetis kepada closure rendering pemanggil. Pemanggil menggambar swatch tanpa mengelola status internal komponen.

Antarmuka apa yang menyatakan persyaratan ini?

Gunakan parameter generik bernama untuk view yang dikembalikan dan tipe konkret untuk input komponen. Dalam contoh di bawah, tanda tangan esensialnya adalah @ViewBuilder content: @escaping (SwatchInput) -> Content, di dalam deklarasi yang dibatasi oleh Content: View.

Input dan output memiliki tugas yang berbeda. SwatchInput menentukan nilai yang disediakan komponen. Content mewakili tipe view konkret yang dihasilkan oleh closure setiap pemanggil. Parameter generik bernama memungkinkan deklarasi menyatakan dan menggunakan kembali hubungan tipe ini, seperti dijelaskan dalam bab Swift Generics.

Apple menggunakan pola antarmuka ini di GeometryReader: initializer-nya menerima closure bertipe (GeometryProxy) -> Content. Kontainer menyediakan proxy, lalu pemanggil membangun konten darinya. Ini merupakan contoh yang berguna bagi komponen khusus yang menyediakan konfigurasinya sendiri. Lihat dokumentasi Apple tentang GeometryReader dan init(content:).

Pilih kontrak sesuai tugas sebenarnya:

Persyaratan Antarmuka yang sesuai
Pemanggil menyediakan konten tanpa input dari komponen () -> Content
Komponen menyediakan konfigurasi tertentu (SwatchInput) -> Content
Kontainer menerima nilai yang tipenya bervariasi di antara penggunaan Parameter generik Input bernama, dengan closure (Input) -> Content yang cocok
Compiler harus membuktikan bahwa pemanggil menggunakan konfigurasi Tanda tangan closure tidak menyatakan persyaratan tersebut

Pilihan ini mengikuti tipe fungsi Swift: tipe parameternya dan tipe hasilnya menjelaskan apa yang dapat diteruskan dan dikembalikan. Keduanya tidak menjelaskan bagaimana implementasi harus menggunakan argumennya. Lihat bagian Function Type di referensi bahasa Swift.

Bangun pratinjau swatch macOS yang lengkap

Gunakan target aplikasi macOS dengan SwiftUI. Contoh ini menggunakan ejaan @ViewBuilder yang sudah mapan dan ditujukan untuk Swift 5.7 atau yang lebih baru, dengan target deployment macOS 12 atau yang lebih baru. Contoh ini tidak menggunakan makro pratinjau maupun nama content builder yang lebih baru. Swift dan Apple SDK tidak tersedia di lingkungan riset, jadi program ini telah diperiksa tetapi tidak dikompilasi atau dijalankan di sini.

Di proyek aplikasi SwiftUI Xcode, ganti deklarasi aplikasi yang dihasilkan dengan program lengkap berikut. Hapus deklarasi ContentView yang dihasilkan jika akan menduplikasi deklarasi ini. Pastikan hanya ada satu titik masuk aplikasi bertanda @main. Bantuan Xcode dari Apple menjelaskan cara memulai dengan proyek SwiftUI atau menambahkan file SwiftUI ke proyek yang sudah ada.

import SwiftUI

struct SwatchInput {
    let title: String
    let side: CGFloat
    let color: Color
}

@MainActor
struct SwatchPanel<Content: View>: View {
    @State private var isLarge = false
    private let content: (SwatchInput) -> Content

    init(
        @ViewBuilder content: @escaping (SwatchInput) -> Content
    ) {
        self.content = content
    }

    private var input: SwatchInput {
        SwatchInput(
            title: "Sample A",
            side: isLarge ? 120 : 64,
            color: .blue
        )
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Toggle("Large sample", isOn: $isLarge)
            content(input)
        }
        .padding(24)
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        SwatchPanel { input in
            Text(input.title)
                .font(.headline)

            RoundedRectangle(cornerRadius: 12)
                .fill(input.color)
                .frame(width: input.side, height: input.side)

            if input.side == 120 {
                Text("Large preview")
                    .font(.caption)
            }
        }
        .frame(width: 300, height: 280, alignment: .topLeading)
    }
}

@main
@MainActor
struct TypedContentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Tampilan awal yang diharapkan berisi toggle Large sample, judul Sample A, dan persegi membulat biru dengan panjang sisi 64 poin. Mengaktifkan toggle mengubah panjang sisi yang disediakan menjadi 120 poin dan menambahkan Large preview. Ukuran dan label ini merupakan nilai ilustratif lokal, bukan hasil pengukuran.

Komponen memiliki isLarge. input yang dihitungnya mengemas pilihan saat ini, dan content(input) meneruskan paket tersebut kepada pemanggil. SwiftUI mengelola penyimpanan status dan memperbarui bagian hierarki view yang bergantung pada nilai yang berubah, sebagaimana didokumentasikan di bagian State.

Pemanggil mengatur penyajian: judul, bentuk, dan keterangan opsional. Pemanggil dapat mengganti susunan tersebut sambil tetap menggunakan input bertipe yang sama. Panel dan pemanggilnya sama-sama mengimplementasikan body untuk menyusun antarmukanya, mengikuti model View yang didokumentasikan Apple.

Mengapa anotasi builder dan escaping diperlukan?

@ViewBuilder memungkinkan pemanggil menulis beberapa ekspresi view dan cabang kondisional yang didukung dalam satu closure. Dalam contoh ini, pemanggil menyediakan dua view tanpa syarat dan satu keterangan kondisional. Builder menggabungkan konten tersebut menjadi hasil yang memenuhi batasan hasil komponen. Apple mendokumentasikan beberapa view anak dan konstruksi kondisional di bagian ViewBuilder.

Builder tidak menyediakan SwatchInput. Parameter closure biasa mendeklarasikan input, dan content(input) menyediakan argumen sebenarnya. Jika initializer menerima () -> Content, menambahkan anotasi builder tidak akan mengubahnya menjadi closure yang menerima konfigurasi.

@escaping diperlukan karena initializer menyimpan closure di sebuah properti untuk dipanggil nanti dari body. Bab Swift Closures menjelaskan bahwa closure bersifat escaping jika dapat dipanggil setelah fungsi penerimanya selesai.

Untuk komponen yang konfigurasinya sudah final saat inisialisasi, alternatif desainnya adalah langsung memanggil builder nonescaping dan menyimpan nilai Content yang dihasilkan. Itu mengubah waktu penerapan konfigurasi. Di sini, mempertahankan closure memperjelas alur yang dimaksud: turunkan input saat ini dari status komponen, lalu bangun konten menggunakan input tersebut.

Pertahankan batas builder Xcode

Dokumentasi Apple saat ini membedakan Xcode 26 dan versi lebih lama dari Xcode 27 dan versi lebih baru. Untuk tool versi lebih lama, dokumentasi tersebut menjelaskan ViewBuilder sebagai builder untuk view. Pada Xcode 27 dan versi lebih baru, dokumentasi menjelaskan konstruksi yang tidak bergantung pada tipe dan merekomendasikan alias ContentBuilder untuk menunjukkan perilaku yang lebih luas itu. Lihat ViewBuilder.

Saat memelihara basis kode Xcode 27 atau yang lebih baru, initializer dapat menggunakan @ContentBuilder. Apple mendokumentasikannya sebagai alias tipe untuk ViewBuilder, dengan konformansi yang ditegakkan melalui tipe konten yang dihasilkannya. Pertahankan Content: View pada komponen ini: outputnya tetap ditujukan untuk hierarki view. Jangan menganggap ejaan yang lebih baru tersedia di SDK lama hanya karena dokumentasi saat ini mencantumkan target deployment yang lebih lama. Lihat ContentBuilder.

Mengapa some View tidak menyelesaikan semua bagian kontrak ini?

some View sesuai untuk properti body dalam contoh ini. Ejaan tersebut menyembunyikan tipe hasil konkret sambil mempertahankan jaminan bahwa hasilnya adalah sebuah view. Sebaliknya, closure yang disimpan menggunakan parameter bernama Content agar komponen dapat merujuk tipe hasil yang sama di properti dan initializer-nya.

Swift 5.7 memperkenalkan deklarasi parameter opak melalui SE-0341. Parameter langsung seperti _ value: some View memperkenalkan parameter generik tanpa nama yang tipe konkretnya ditentukan oleh pemanggil. Artinya bukan implementasi fungsi dapat memilih view sembarang lalu meneruskannya ke mana pun tipe tanpa nama tersebut diharapkan. Lihat Opaque Parameter Declarations.

Secara khusus, parameter callback yang ditulis sebagai (some View) -> Content berada di luar posisi parameter opak yang didukung dan didokumentasikan oleh SE-0341. Proposal tersebut secara eksplisit melarang tipe opak pada posisi input tipe fungsi callback. Pembahasannya tentang kemungkinan mengaktifkan sintaks tersebut terdapat di bagian arah mendatang, bukan perilaku yang sudah diterapkan. Referensi Types saat ini tetap mencantumkan batasan tersebut.

Untuk tugas desktop ini, jalur yang didukung sederhana: jadikan nilai yang disediakan komponen sebagai tipe konfigurasi konkret. Jika kontainer lain yang dapat digunakan kembali harus mempertahankan tipe input yang diberikan pemanggil, beri nama tipe itu dengan parameter generik dan gunakan secara konsisten pada nilai yang disimpan serta callback. Kedua desain tersebut tidak memerlukan input callback opak.

Apa yang sebenarnya dijamin compiler?

Tanda tangan tersebut menjamin bentuk yang dapat dipanggil: satu argumen SwatchInput dan hasil yang memenuhi Content: View. Tanda tangan itu juga memberi parameter closure tipe hasil inferensi, sehingga pemanggil dapat mengakses input.title, input.side, dan input.color tanpa mengulang anotasi tipe. Swift mendokumentasikan inferensi dari konteks closure ini di bagian Closures.

Tanda tangan itu tidak dapat menjamin bahwa properti tersebut memengaruhi tampilan. Sebagai contoh, berikut ekspresi pengganti yang valid di dalam ContentView.body; ekspresi ini sengaja menerima dan mengabaikan konfigurasi:

SwatchPanel { _ in
    Text("Fixed preview")
}

Batas ini penting saat menulis persyaratan komponen. “Konten pemanggil menerima SwatchInput” adalah kontrak tipe. “Konten pemanggil menampilkan ukuran yang disediakan” adalah persyaratan perilaku. Jika menampilkan ukuran tersebut merupakan hal penting bagi komponen, letakkan rendering itu di dalam komponen dan sediakan area kustomisasi yang lebih kecil.

Gunakan pemeriksaan berikut untuk mendiagnosis masalah integrasi. Ini adalah langkah pemeriksaan yang diturunkan dari antarmuka, bukan hasil pengujian compiler yang dilaporkan:

Gejala Periksa
Argumen hilang saat memanggil content Teruskan konfigurasi: content(input).
Diagnostik closure escaping di initializer Pertahankan @escaping saat menyimpan callback.
Beberapa view bersaudara gagal di pemanggil Pertahankan anotasi builder yang sesuai pada parameter initializer.
Callback bertipe eksplisit ditolak Pastikan inputnya adalah SwatchInput, bukan tipe yang tidak terkait seperti String.
Pratinjau mengabaikan toggle ukuran Periksa apakah rendering membaca input.side.
Komponen terus menampilkan konfigurasi awal Periksa apakah konten dibuat sekali lalu disimpan, alih-alih memanggil callback dengan input terkini.

Pertahankan kontrak sesederhana yang diizinkan tugas. Konfigurasi konkret dan hasil view generik memberi panel swatch ini batas yang jelas: komponen menyediakan nilai saat ini, pemanggil menerimanya dengan informasi tipe lengkap, dan penyajian yang dipilih tetap mudah diganti.

Sumber