Untuk panel SwiftUI yang dapat digunakan kembali di aplikasi Mac, letakkan builder konten utama di urutan pertama, beri label pada builder lainnya, dan gunakan inisialisator dengan batasan untuk bagian yang dihilangkan. Ini membuat pemanggilan mudah dibaca dan menetapkan tipe eksplisit untuk header dan footer yang tidak ada.

Tutorial ini membuat komponen kecil dengan bagian konten, header, dan footer. Tujuannya adalah mendukung empat pemanggilan yang berguna—hanya konten, konten dengan salah satu aksesori, serta konten dengan keduanya—tanpa bergantung pada closure default yang saling bersaing. Semua nilai yang ditampilkan adalah teks lokal sintetis.

Apa yang membuat inisialisator dengan beberapa builder tidak ambigu?

Sintaks beberapa trailing closure di Swift menghilangkan label argumen pada trailing closure pertama dan mewajibkan label pada trailing closure berikutnya. Karena itu, komponen sebaiknya membuat tujuan builder pertama jelas dari nama komponennya dan memberi builder selanjutnya label yang berbeda. Sintaks ini diterapkan di Swift 5.3. SE-0279: Multiple Trailing Closures

Untuk panel ini, inisialisator lengkapnya adalah init(content:header:footer:). Di lokasi pemanggilan, blok pertama menyediakan konten; header: dan footer: mengidentifikasi blok lainnya. Urutan deklarasi mengatur urutan argumen, sedangkan body komponen mengatur urutan visual. Jadi, konten dapat menjadi argumen pertama meskipun header tampil di atasnya.

Antarmuka Section Apple saat ini mengikuti susunan yang mengutamakan konten ini. Inisialisator lengkapnya menerima tiga closure builder, dan inisialisator terpisah mendukung kombinasi dengan bagian yang tidak ada. Ini merupakan acuan antarmuka yang berguna; panel khusus di bawah ini memiliki tata letak dan perilakunya sendiri. Apple: Section, Apple: init(content:header:footer:)

Atribut builder dan label parameter menyelesaikan masalah yang berbeda. Atribut result builder mentransformasi isi closure yang diberikan; label argumen menentukan cara pemanggil mengidentifikasi parameter. Menambahkan @ViewBuilder tidak mengembalikan label pertama ketika sintaks trailing closure menghilangkannya. SE-0289: Result builders

Perbedaan itu menghasilkan aturan desain praktis: hindari inisialisator bersaudara yang satu-satunya pembeda bergunanya hilang ketika label trailing closure pertama dihapus. Misalnya, dua inisialisator yang selain itu setara, bernama init(content:header:) dan init(body:header:), akan menampilkan blok pertama tanpa label yang sama, diikuti header:. Ini merupakan konsekuensi desain dari aturan trailing closure, bukan alasan untuk menambahkan lebih banyak anotasi builder.

Batas compiler dan SwiftUI mana yang perlu diperhatikan?

Perhatikan tiga hal: compiler, mode bahasa Swift yang dipilih, dan SDK SwiftUI. Beberapa trailing closure hadir di Swift 5.3, sedangkan fitur result builder resmi diterapkan di Swift 5.4. Tonggak tersebut menjelaskan dukungan bahasa; itu tidak menetapkan ketersediaan untuk setiap inisialisator framework. SE-0279, SE-0289

Dokumentasi Apple saat ini juga menetapkan batas toolchain: Xcode 26 dan versi sebelumnya menggunakan perilaku pembuatan view yang sudah mapan, sedangkan Xcode 27 dan versi setelahnya membangun konten yang tidak bergantung pada tipe dari builder ini. Untuk perilaku yang lebih baru, Apple mengarahkan pengembang ke penulisan @ContentBuilder. ContentBuilder adalah alias tipe untuk ViewBuilder; batasan API yang menaunginya tetap menentukan konten mana yang dapat diterima. Apple: ViewBuilder, Apple: ContentBuilder

Contoh lengkap di bawah menggunakan penulisan @ViewBuilder untuk target Xcode 26 dan macOS 11 atau versi lebih baru. Saat mengadopsi gaya antarmuka Xcode 27 yang terdokumentasi, ganti anotasi parameter inisialisator dengan @ContentBuilder. Pertahankan tiga batasan generik View: komponen ini menempatkan ketiga bagian di dalam tata letak view.

Hindari menyebut tipe tuple yang dihasilkan builder dalam kontrak komponen. Apple mendokumentasikan inkompatibilitas sumber yang melibatkan batasan TupleView eksplisit saat mengadopsi implementasi builder yang lebih baru. Tipe bagian generik memungkinkan panel ini menghindari ketergantungan tersebut. Apple: TN3211, Resolving SwiftUI source incompatibilities for State and ContentBuilder

Buat panel dengan empat bentuk inisialisator eksplisit

Gunakan tipe generik yang terpisah untuk setiap bagian. Menggunakan ulang satu parameter generik akan mengharuskan bagian-bagian tersebut memiliki tipe konkret yang sama, suatu batasan yang tidak perlu ketika satu bagian berisi satu view teks dan bagian lain berisi beberapa view.

Untuk bagian yang tidak ada, khususkan parameter generiknya menjadi EmptyView. Apple mendokumentasikan peran ini secara persis untuk EmptyView: mewakili child yang tidak ada dalam tipe view generik. Inisialisator dengan batasan di bawah menetapkan pilihan itu secara eksplisit alih-alih meminta pemanggil menyimpulkan tipe aksesori yang tidak ditentukan. Apple: EmptyView

Dua blok Swift di bagian ini membentuk satu file aplikasi lengkap. Dalam target aplikasi macOS SwiftUI, ganti deklarasi aplikasi dan content view yang dibuat otomatis dengan blok-blok ini, dan pertahankan tepat satu deklarasi @main. Blok pertama mendefinisikan komponen dan keempat bentuk inisialisatornya.

import SwiftUI

struct BuilderPanel<Content: View, Header: View, Footer: View>: View {
    private let content: Content
    private let header: Header
    private let footer: Footer

    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder header: () -> Header,
        @ViewBuilder footer: () -> Footer
    ) {
        self.content = content()
        self.header = header()
        self.footer = footer()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            header
            content
            footer
        }
        .padding()
    }
}

extension BuilderPanel where Footer == EmptyView {
    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder header: () -> Header
    ) {
        self.init(
            content: content,
            header: header,
            footer: { EmptyView() }
        )
    }
}

extension BuilderPanel where Header == EmptyView {
    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder footer: () -> Footer
    ) {
        self.init(
            content: content,
            header: { EmptyView() },
            footer: footer
        )
    }
}

extension BuilderPanel where Header == EmptyView, Footer == EmptyView {
    init(@ViewBuilder content: () -> Content) {
        self.init(
            content: content,
            header: { EmptyView() },
            footer: { EmptyView() }
        )
    }
}

Inisialisator lengkap memanggil setiap builder sekali selama inisialisasi nilai panel tersebut dan menyimpan nilai yang dikembalikan. Inisialisator praktis mendelegasikannya ke inisialisator itu. Waktu pemanggilan ini merupakan sifat implementasi ini: hal ini tidak menjanjikan bahwa builder hanya berjalan sekali selama masa pakai antarmuka, karena pembuatan nilai panel lain akan memanggil inisialisatornya lagi.

Tambahkan blok kedua ini ke file yang sama. Blok ini menguji setiap bentuk yang didukung, termasuk beberapa pernyataan dalam satu builder.

struct PanelDemo: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                BuilderPanel {
                    Text("Canvas A")
                }

                BuilderPanel {
                    Text("Canvas B")
                    Text("Grid: 12 columns")
                } header: {
                    Text("Layout")
                }

                BuilderPanel {
                    Text("Canvas C")
                } footer: {
                    Text("Units: points")
                }

                BuilderPanel {
                    Text("Canvas D")
                } header: {
                    Text("Preview")
                } footer: {
                    Text("Scale: 100 percent")
                }
            }
            .padding()
        }
        .frame(minWidth: 360, minHeight: 440)
    }
}

@main
struct BuilderPanelDemoApp: App {
    var body: some Scene {
        WindowGroup {
            PanelDemo()
        }
    }
}

Bangun dan jalankan aplikasi di Xcode untuk memvalidasi kontrak terhadap SDK yang dipilih. Listing ini telah diperiksa, tetapi tidak dikompilasi atau dijalankan di sini; lingkungan riset tidak memiliki compiler Swift maupun SDK macOS SwiftUI.

Cocokkan setiap pemanggilan dengan kontrak tipe konkret

Tabel bentuk pemanggilan merupakan panduan praktis. Tabel ini menyatakan inisialisator mana yang seharusnya dipilih dan bagian yang tidak ada akan menerima tipe konkret apa. Semua ini adalah ekspektasi yang diturunkan dari deklarasi contoh dan siap diperiksa di proyek Anda.

Bagian yang disediakan Label inisialisator Batasan bagian yang tidak ada
Hanya konten content: Header == EmptyView, Footer == EmptyView
Konten dan header content:header: Footer == EmptyView
Konten dan footer content:footer: Header == EmptyView
Ketiganya content:header:footer: Tidak ada

Setiap inisialisator yang lebih ringkas mewajibkan semua closure yang dideklarasikannya. Tidak ada argumen default yang membuat inisialisator tiga builder bersaing untuk pemanggilan yang hanya berisi konten. Bentuk aksesori yang hanya memiliki header dan yang hanya memiliki footer dibedakan lewat label kedua, meskipun kedua closure menghasilkan Text.

Gunakan tabel ini saat mengubah komponen: jika inisialisator baru menerima bentuk pemanggilan yang sudah ada, periksa apakah perbedaan baru itu tetap terlihat setelah label trailing pertama dihilangkan. Ini membantu menemukan tumpang tindih antarmuka sebelum berkembang menjadi diagnostik yang sulit dalam view yang lebih besar.

Mengapa menghindari beberapa closure builder dengan nilai default?

Argumen default berguna, tetapi beberapa parameter closure dengan nilai default memperkenalkan aturan pencocokan yang bergantung pada mode bahasa Swift. SE-0286 mendokumentasikan pencocokan maju, heuristik kompatibilitas untuk melewati parameter default, dan fallback pencocokan mundur dalam mode yang lebih lama. Dokumen itu juga menjelaskan flag fitur mendatang ForwardTrailingClosures, yang diterapkan di Swift 5.8. SE-0286: Forward-scan matching for trailing closures

Rekomendasi praktis di sini memang lebih terbatas daripada aturan-aturan tersebut: panel kecil yang dapat digunakan kembali bisa menghindari ketergantungan padanya. Wajibkan builder utama dan nyatakan penghilangan yang didukung melalui inisialisator dengan batasan. Dengan begitu, kombinasi yang didukung terlihat dalam deklarasi dan perilaku pencocokan yang sensitif terhadap versi compiler tidak dijadikan bagian dari makna yang dimaksudkan untuk komponen.

Aksesori yang dihilangkan dan closure aksesori yang kosong juga merupakan pilihan antarmuka yang berbeda. Inisialisator yang hanya berisi konten secara eksplisit memilih EmptyView untuk kedua aksesori. Memberikan builder kosong meminta implementasi builder SDK menentukan hasil closure tersebut. Untuk komponen ini, hilangkan bagian yang tidak digunakan dan biarkan inisialisator praktis menyediakan tipe ketidakhadiran yang terdokumentasi.

Pendekatan ini memiliki batas skala. Tiga bagian menghasilkan empat kombinasi berguna ketika konten selalu diwajibkan. Komponen dengan banyak bagian yang masing-masing opsional akan memerlukan keluarga overload yang jauh lebih besar. Sebelum menambahkan overload tersebut, tentukan kombinasi mana yang benar-benar dibutuhkan pembaca; sejumlah kecil varian komponen yang bermakna mungkin lebih mudah digunakan dan dipelihara.

Bagaimana cara mendiagnosis pemanggilan yang ambigu?

Mulailah dari pemanggilan panel terkecil yang mereproduksi masalah. Lalu ikuti urutan ini; cara ini memisahkan pencocokan argumen dari kegagalan di dalam builder.

  1. Tulis semua argumen closure di dalam tanda kurung. Tuliskan content:, header:, dan footer: sesuai urutan deklarasi. Jika pemanggilan berhasil, periksa perbedaan mana yang dihilangkan oleh sintaks trailing.
  2. Ganti sementara setiap bagian dengan satu ekspresi Text. Jika pemanggilan berhasil, kembalikan ekspresi asli satu per satu untuk menemukan isi builder yang gagal.
  3. Bandingkan bentuk inisialisator. Cari nilai default yang bersaing, urutan closure yang terbalik, dan overload yang hanya dibedakan oleh label builder pertama.
  4. Periksa batasan bagian yang tidak ada. Setiap bagian generik yang dihilangkan seharusnya memiliki tipe konkret yang dimaksud dalam inisialisator yang dipilih.
  5. Periksa batas toolchain. Setelah beralih ke Xcode 27, periksa batasan hasil builder eksplisit dan nama tipe yang bertabrakan sebelum merancang ulang keluarga inisialisator.

Menuliskan label di dalam tanda kurung mempertahankan nama argumen yang dihilangkan oleh trailing closure pertama. Swift mendukung kedua bentuk, jadi bentuk berlabel lengkap berguna untuk diagnostik dan juga cocok sebagai penulisan akhir ketika membuat pemanggilan tertentu lebih mudah dibaca. The Swift Programming Language: Closures

Untuk diagnostik SDK yang lebih baru, Apple secara khusus menyarankan penggunaan nama tipe berkualifikasi penuh untuk tipe yang bertabrakan antar modul dan memperbarui batasan yang bergantung pada tipe output tuple lama. Masalah ini dapat terjadi di dalam pemanggilan inisialisator yang desainnya sudah baik; menambahkan lebih banyak overload tidak akan menyelesaikan masalah mendasarnya. Apple: TN3211

Kontrak inisialisator yang jelas dimulai dari pemanggilan yang harus didukung komponen. Letakkan konten utamanya di urutan pertama, beri label yang bermakna pada builder aksesori, dan tetapkan tipe eksplisit untuk bagian yang tidak ada. Lalu bangun setiap bentuk pemanggilan yang didukung dengan compiler, mode bahasa, dan SDK yang Anda pilih sebelum memperluas antarmuka.

Sumber