Untuk panel SwiftUI yang boleh digunakan semula dalam aplikasi Mac, letakkan builder kandungan utama dahulu, labelkan builder yang selebihnya dan gunakan pemula terkekang untuk bahagian yang tidak disertakan. Ini menjadikan panggilan mudah dibaca dan memberikan jenis yang jelas kepada pengepala dan pengaki yang tiada.

Tutorial ini membina komponen kecil dengan bahagian kandungan, pengepala dan pengaki. Matlamatnya ialah menyokong empat bentuk panggilan yang berguna—kandungan sahaja, kandungan dengan salah satu aksesori, serta kandungan dengan kedua-duanya—tanpa bergantung pada closure lalai yang bersaing. Semua nilai yang dipaparkan ialah teks setempat sintetik.

Apakah yang menjadikan pemula berbilang builder tidak kabur?

Sintaks berbilang closure hujung Swift menggugurkan label argumen bagi closure hujung pertama dan memerlukan label untuk closure hujung yang seterusnya. Oleh itu, komponen patut menjadikan tujuan builder pertamanya jelas melalui nama komponen dan memberikan label berbeza kepada builder yang berikutnya. Sintaks ini dilaksanakan dalam Swift 5.3. SE-0279: Multiple Trailing Closures

Untuk panel ini, pemula penuh ialah init(content:header:footer:). Di tapak panggilan, blok pertama membekalkan kandungan; header: dan footer: mengenal pasti blok lain. Tertib pengisytiharannya menentukan tertib argumen, manakala body komponen menentukan tertib visual. Oleh itu, kandungan boleh menjadi argumen pertama walaupun pengepala muncul di atasnya.

Antara muka Section Apple yang terkini mengikuti susunan yang mengutamakan kandungan ini. Pemula penuhnya menerima tiga closure builder, dan pemula berasingan menyokong gabungan dengan bahagian yang tiada. Ini contoh rujukan antara muka yang berguna; panel tersuai di bawah mempunyai susun atur dan tingkah lakunya sendiri. Apple: Section, Apple: init(content:header:footer:)

Atribut builder dan label parameter menyelesaikan masalah yang berbeza. Atribut result-builder mengubah badan closure yang dibekalkan; label argumen menentukan cara pemanggil mengenal pasti parameter. Menambah @ViewBuilder tidak mengembalikan label pertama apabila sintaks closure hujung menggugurkannya. SE-0289: Result builders

Perbezaan itu memberikan aturan reka bentuk yang praktikal: elakkan pemula setara yang hanya perbezaannya yang berguna hilang apabila label closure hujung pertama digugurkan. Contohnya, dua pemula yang selainnya setara bernama init(content:header:) dan init(body:header:) akan memaparkan blok pertama tanpa label yang sama, diikuti header:. Ini kesan daripada aturan closure hujung, bukannya alasan untuk menambah lebih banyak anotasi builder.

Apakah sempadan compiler dan SwiftUI yang penting?

Ambil kira tiga perkara: compiler, mod bahasa Swift yang dipilih dan SDK SwiftUI. Berbilang closure hujung diperkenalkan dalam Swift 5.3, manakala ciri result-builder rasmi dilaksanakan dalam Swift 5.4. Pencapaian ini menerangkan sokongan bahasa; ia tidak menetapkan ketersediaan bagi setiap pemula rangka kerja. SE-0279, SE-0289

Dokumentasi terkini Apple juga menetapkan sempadan toolchain: Xcode 26 dan versi terdahulu menggunakan tingkah laku pembinaan view yang mantap, manakala Xcode 27 dan versi seterusnya membina kandungan tanpa mengira jenis daripada builder ini. Untuk tingkah laku yang lebih baharu, Apple mengarahkan pembangun menggunakan ejaan @ContentBuilder. ContentBuilder ialah alias jenis untuk ViewBuilder; kekangan API yang menempatkannya tetap menentukan kandungan yang boleh diterima. Apple: ViewBuilder, Apple: ContentBuilder

Sampel lengkap di bawah menggunakan ejaan @ViewBuilder sebagai garis dasar Xcode 26 dan menyasarkan macOS 11 atau versi seterusnya. Apabila menggunakan gaya antara muka Xcode 27 yang didokumenkan, gantikan anotasi parameter pemula dengan @ContentBuilder. Kekalkan tiga kekangan generik View: komponen ini meletakkan ketiga-tiga bahagian dalam susun atur view.

Elakkan daripada menamakan jenis tuple yang dijana builder dalam kontrak komponen. Apple mendokumenkan ketidakserasian sumber yang melibatkan kekangan TupleView eksplisit apabila menggunakan pelaksanaan builder yang lebih baharu. Jenis rantau generik membolehkan panel ini mengelakkan kebergantungan tersebut. Apple: TN3211, Resolving SwiftUI source incompatibilities for State and ContentBuilder

Bina panel dengan empat bentuk pemula yang jelas

Gunakan jenis generik yang berasingan untuk setiap bahagian. Menggunakan semula satu parameter generik akan menghendaki semua bahagian mempunyai jenis konkrit yang sama, iaitu sekatan yang tidak perlu apabila satu bahagian mengandungi satu view teks dan bahagian lain mengandungi beberapa view.

Untuk bahagian yang tiada, khususkan parameter generiknya kepada EmptyView. Apple mendokumenkan peranan tepat ini bagi EmptyView: mewakili anak yang tiada dalam jenis view generik. Pemula terkekang di bawah menjadikan pilihan itu jelas dan tidak meminta pemanggil membuat inferens bagi jenis aksesori yang tidak dinyatakan. Apple: EmptyView

Dua blok Swift dalam bahagian ini membentuk satu fail aplikasi lengkap. Dalam sasaran aplikasi SwiftUI macOS, gantikan pengisytiharan aplikasi dan paparan kandungan yang dijana dengan blok ini, sambil mengekalkan tepat satu pengisytiharan @main. Blok pertama mentakrifkan komponen dan keempat-empat bentuk pemula.

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() }
        )
    }
}

Pemula penuh memanggil setiap builder sekali semasa pemulaan nilai panel itu dan menyimpan nilai yang dikembalikan. Pemula kemudahan mendelegasikan kepada pemula penuh tersebut. Masa pelaksanaan ini ialah sifat pelaksanaan ini: ia tidak menjanjikan builder hanya berjalan sekali sepanjang hayat antara muka, kerana penciptaan nilai panel yang lain akan memanggil pemulanya sekali lagi.

Tambahkan blok kedua ini pada fail yang sama. Blok ini menguji setiap bentuk yang disokong, termasuk berbilang 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()
        }
    }
}

Bina dan jalankan aplikasi dalam Xcode untuk mengesahkan kontrak terhadap SDK yang dipilih. Penyenaraian ini telah diperiksa tetapi tidak dikompil atau dilaksanakan di sini; persekitaran penyelidikan tidak mempunyai compiler Swift mahupun SDK SwiftUI macOS.

Padankan setiap panggilan dengan kontrak jenis konkrit

Jadual bentuk panggilan ialah panduan keputusan yang berguna. Jadual ini menyatakan pemula yang patut dipilih dan bahagian yang tiada menerima jenis konkrit. Ini jangkaan yang diperoleh daripada pengisytiharan sampel, sedia untuk disemak dalam projek anda.

Bahagian yang dibekalkan Label pemula Kekangan bahagian yang tiada
Kandungan sahaja content: Header == EmptyView, Footer == EmptyView
Kandungan dan pengepala content:header: Footer == EmptyView
Kandungan dan pengaki content:footer: Header == EmptyView
Ketiga-tiganya content:header:footer: Tiada

Setiap pemula yang lebih ringkas memerlukan semua closure yang diisytiharkannya. Tiada argumen lalai yang menyebabkan pemula tiga builder bersaing untuk panggilan kandungan sahaja. Bentuk aksesori pengepala sahaja dan pengaki sahaja dibezakan oleh label kedua masing-masing, walaupun kedua-dua closure mengembalikan Text.

Gunakan jadual ini apabila mengubah komponen: jika pemula baharu menerima bentuk panggilan sedia ada, semak sama ada perbezaan baharu itu masih jelas selepas label closure hujung pertama digugurkan. Ini mengesan pertindihan antara muka sebelum ia menghasilkan diagnostik yang sukar dalam view yang lebih besar.

Mengapa elakkan beberapa closure builder lalai?

Argumen lalai berguna, tetapi beberapa parameter closure dengan nilai lalai memperkenalkan aturan pemadanan yang bergantung pada mod bahasa Swift. SE-0286 mendokumenkan pemadanan ke hadapan, heuristik keserasian untuk melangkau parameter lalai, serta kaedah sandaran pemadanan ke belakang dalam mod lama. Dokumen itu turut menerangkan bendera ciri akan datang ForwardTrailingClosures, yang dilaksanakan dalam Swift 5.8. SE-0286: Forward-scan matching for trailing closures

Saranan praktikal di sini sengaja lebih khusus daripada aturan tersebut: panel kecil yang boleh digunakan semula boleh mengelakkan kebergantungan padanya. Jadikan builder utamanya wajib dan nyatakan bahagian yang boleh ditinggalkan melalui pemula terkekang. Ini menjadikan gabungan yang disokong jelas dalam pengisytiharan dan mengelakkan penggunaan tingkah laku pemadanan yang sensitif kepada versi compiler sebagai sebahagian daripada maksud komponen.

Aksesori yang ditinggalkan dan closure aksesori yang kosong juga merupakan pilihan antara muka yang berbeza. Pemula kandungan sahaja memilih EmptyView secara jelas untuk kedua-dua aksesori. Membekalkan builder kosong meminta pelaksanaan builder SDK menentukan hasil closure itu. Bagi komponen ini, tinggalkan bahagian yang tidak digunakan dan biarkan pemula kemudahan membekalkan jenis ketiadaan yang didokumenkan.

Pendekatan ini ada had dari segi skala. Tiga bahagian menghasilkan empat gabungan berguna apabila kandungan sentiasa diperlukan. Komponen dengan banyak bahagian yang masing-masing boleh ditinggalkan memerlukan keluarga overload yang jauh lebih besar. Sebelum menambah overload tersebut, tentukan gabungan yang benar-benar diperlukan oleh pembaca; beberapa varian komponen yang lebih bermakna mungkin lebih mudah digunakan dan diselenggarakan.

Bagaimanakah anda mendiagnosis panggilan yang kabur?

Mulakan dengan panggilan panel paling kecil yang menghasilkan semula masalah. Kemudian ikuti urutan ini; ia memisahkan pemadanan argumen daripada kegagalan dalam builder.

  1. Tulis semua argumen closure di dalam kurungan. Nyatakan content:, header: dan footer: mengikut tertib pengisytiharan. Jika ini menyelesaikan panggilan, periksa perbezaan yang digugurkan oleh sintaks closure hujung.
  2. Gantikan setiap bahagian buat sementara dengan satu ungkapan Text. Jika panggilan itu berfungsi, pulihkan ungkapan asal satu demi satu untuk mencari badan builder yang gagal.
  3. Bandingkan bentuk pemula. Cari nilai lalai yang bersaing, tertib closure terbalik dan overload yang hanya dibezakan oleh label builder pertama.
  4. Semak kekangan bahagian yang tiada. Setiap bahagian generik yang ditinggalkan patut mempunyai jenis konkrit yang dimaksudkan dalam pemula yang dipilih.
  5. Semak sempadan toolchain. Selepas beralih kepada Xcode 27, periksa kekangan hasil builder yang eksplisit dan nama jenis yang bertembung sebelum mereka bentuk semula keluarga pemula.

Menulis label di dalam kurungan mengekalkan nama argumen yang digugurkan oleh closure hujung pertama. Swift menyokong kedua-dua bentuk, jadi bentuk berlabel penuh berguna untuk diagnosis dan sesuai sebagai ejaan akhir apabila menjadikan sesuatu panggilan lebih mudah dibaca. The Swift Programming Language: Closures

Bagi diagnostik SDK yang lebih baharu, Apple secara khusus mengesyorkan supaya jenis yang bertembung antara modul dinyatakan dengan kelayakan dan kekangan yang bergantung pada jenis output tuple lama dikemas kini. Masalah ini boleh berlaku dalam panggilan pemula yang direka dengan baik; menambah lebih banyak overload tidak akan menyelesaikan masalah asasnya. Apple: TN3211

Kontrak pemula yang jelas bermula dengan panggilan yang perlu disokong oleh komponen. Letakkan kandungan utama dahulu, berikan label yang bermakna kepada builder aksesori dan tetapkan jenis yang jelas kepada bahagian yang tiada. Kemudian bina setiap bentuk panggilan yang disokong menggunakan compiler, mod bahasa dan SDK pilihan anda sebelum memperluas antara muka.

Sumber