Jadikan komponen SwiftUI anda generik berdasarkan jenis yang mematuhi protokol itu, kemudian isytiharkan paparan tersimpannya sebagai Provider.Content. Ini mengekalkan hubungan antara pembekal dengan jenis paparannya yang berkaitan di seluruh komponen.

Tutorial ini membina tetingkap macOS kecil yang mengandungi dua jubin sintetik. Tugasnya ialah menyimpan pembekal dan paparan yang dihasilkannya tanpa menggantikan jenis berkaitan itu dengan any View atau AnyView. Corak yang sama boleh digunakan apabila komponen desktop boleh guna semula menerima label, aksesori atau pembekal kandungan tersuai.

Apakah hubungan yang perlu dikekalkan oleh sifat tersimpan?

Jenis berkaitan menamakan jenis yang dipilih oleh setiap pematuhan protokol. Jika protokol mengisytiharkan associatedtype Content: View, pembekal yang mematuhinya mesti menyediakan satu jenis kandungan tertentu. Kod generik boleh merujuk jenis itu sebagai Provider.Content, walaupun pelaksanaannya disembunyikan di sebalik hasil legap. Swift menerangkan jenis berkaitan dan kekangan generik dalam Bab Generik.

Oleh itu, komponen memerlukan dua pengisytiharan yang saling berkaitan: parameter generik yang dikekang kepada protokol pembekal, dan sifat kandungan tersimpan yang jenisnya ialah jenis berkaitan bagi parameter itu. Mengisytiharkan dua sifat berasingan yang sekadar mematuhi protokol tidak menyatakan hubungan ini.

SwiftUI sendiri menggunakan kontrak yang berkaitan: View mengisytiharkan associatedtype Body: View, dan Swift membuat inferens jenis berkaitan itu daripada pelaksanaan body paparan tersuai. Content pembekal anda dan Body komponen ialah jenis berkaitan yang berasingan; kedua-duanya tidak perlu sama. Lihat dokumentasi Body daripada Apple.

Bina contoh macOS yang lengkap

Gunakan projek aplikasi macOS Xcode dengan SwiftUI dan sasaran penggunaan macOS 13 atau lebih baharu untuk contoh ini. Gantikan pengisytiharan aplikasi dan paparan kandungan yang dijana dengan fail tunggal berikut, dan pastikan hanya terdapat satu pengisytiharan @main dalam sasaran. Apple menerangkan struktur titik masuk aplikasi ini dan penggunaan WindowGroup dalam App.

Contoh ini sengaja menggunakan sintaks jenis berkaitan dan generik bernama yang telah lama tersedia. Hasil legap dilaksanakan dalam Swift 5.1; sintaks pilihan jenis berkaitan utama yang dibincangkan kemudian memerlukan Swift 5.7. Fail lengkap ini disediakan untuk toolchain Swift 6 semasa. Fail ini disemak berdasarkan dokumentasi yang diperoleh, tetapi tidak dikompil di sini kerana persekitaran ini tidak mempunyai pengkompil Swift. Sasaran penggunaan macOS yang dipilih ialah prasyarat contoh, bukannya dakwaan tentang OS terawal yang menyokong setiap ciri asas. Lihat SE-0244 dan SE-0346.

import SwiftUI

@MainActor
protocol TileProvider {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

@MainActor
struct TextTile: TileProvider {
    let title: String
    let message: String

    func makeContent() -> Text {
        Text(message)
    }
}

@MainActor
struct CircleTile: TileProvider {
    let title: String
    let caption: String

    func makeContent() -> some View {
        HStack(spacing: 8) {
            Circle()
                .fill(Color.blue)
                .frame(width: 16, height: 16)
            Text(caption)
        }
    }
}

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider
    private let content: Provider.Content

    init(provider: Provider) {
        self.provider = provider
        self.content = provider.makeContent()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            content
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        VStack(spacing: 12) {
            TileCard(provider: TextTile(
                title: "Text tile",
                message: "Blue square"
            ))
            TileCard(provider: CircleTile(
                title: "Shape tile",
                caption: "Blue circle"
            ))
        }
        .padding()
        .frame(width: 360)
    }
}

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

Bina dan jalankan sasaran macOS. Hasil yang dijangka ialah tetingkap yang mengandungi jubin teks dan jubin dengan bulatan biru di sebelah kapsyennya. Kedua-duanya menggunakan komponen generik yang sama, dengan jenis pembekal diinferenskan secara berasingan pada setiap tapak panggilan.

Anotasi @MainActor yang jelas memastikan pengisytiharan yang menghasilkan UI ini berada dalam konteks aktor yang sama. Dokumentasi View semasa daripada Apple mengisytiharkan protokol itu dengan pengasingan aktor utama. Anotasi di sini turut menjelaskan konteks yang dimaksudkan bagi protokol pembekal tersuai.

Mengapakah komponen generik mengekalkan jenis itu?

Bagi kad pertama, Provider ialah TextTile, jadi Provider.Content ialah Text. Bagi kad kedua, Provider ialah CircleTile, dan jenis kandungan berkaitannya ialah hasil legap daripada CircleTile.makeContent(). Anda boleh menamakan jenis itu melalui CircleTile.Content tanpa perlu menulis jenis reka letak SwiftUI bersarang.

Penetapan pentingnya ialah self.content = provider.makeContent(). Protokol menjamin bahawa kaedah ini mengembalikan tepat Provider.Content, iaitu jenis yang sama seperti jenis yang diisytiharkan untuk sifat tersimpan. Ini ialah hubungan masa kompilasi yang diperoleh daripada kekangan generik, bukannya perbandingan jenis masa jalan. Bab Generik Swift menerangkan cara mengakses jenis berkaitan melalui parameter generik terkekang.

Penggunaan some View dalam pelaksanaan yang mematuhi protokol tidak menghapuskan hubungan ini. Hasil legap menyembunyikan pelaksanaan konkrit sambil mengekalkan identiti jenisnya. Oleh itu, ia sesuai dijadikan saksi jenis berkaitan. Swift menerangkan perbezaan ini dalam Jenis Protokol Legap dan Berkotak.

Kad-kad ini mempunyai pengkhususan yang berbeza: TileCard<TextTile> dan TileCard<CircleTile>. Penggunaan semula pengisytiharan sumber tidak memerlukan setiap kad mempunyai satu jenis kandungan tersimpan sejagat.

Mengapakah sifat legap atau eksistensial yang berasingan tidak menyelesaikannya?

Berikut ialah cebisan diagnostik, bukannya pengganti bagi program lengkap:

// Diagnostic fragment: no initializer establishes the opaque type.
let content: some View

// Diagnostic fragment: hides which conforming provider is stored.
let provider: any TileProvider

// Diagnostic fragment: stores an existential view value.
let content: any View

Sifat tersimpan legap memerlukan pemula pengisytiharan yang menetapkan jenis asasnya. Menulis some View tidak mewujudkan parameter generik pilihan pemanggil yang boleh diisi kemudian oleh pemula komponen sewenang-wenangnya. Contohnya, let content: some View = Text("Sample") menetapkan jenis bagi pengisytiharan itu; ia tidak menghubungkan sifat tersebut dengan jenis berkaitan sifat yang lain. Peraturan sifat tersimpan ini didokumenkan dalam SE-0244: Jenis Hasil Legap.

Eksistensial seperti any TileProvider membenarkan penyimpanan jenis pembekal berlainan yang mematuhi protokol. Ia menyembunyikan jenis pematuhan tertentu, jadi pengisytiharan itu tidak lagi mendedahkan hubungan sebagai Provider.Content. Begitu juga, any View ialah bekas eksistensial; kontraknya tidak sama dengan jenis generik bernama yang mematuhi View. Lihat penerangan Swift tentang jenis protokol berkotak.

Swift boleh membuka argumen eksistensial secara tersirat untuk sesetengah panggilan generik. Namun, itu tidak bermakna setiap eksistensial boleh digunakan dalam setiap pemula generik. Khususnya, SE-0352 mengehadkan pembukaan apabila jenis asas yang tidak diketahui akan terlepas keluar dalam hasil generik seperti X<T>. Pengkhususan komponen generik boleh menemui batasan ini. Kekalkan pembekal sebagai jenis generik sebelum membina kad, dan jangan anggap eksistensial sentiasa boleh ditukar semula menjadi pengkhususan kekal. Lihat SE-0352: Eksistensial yang Dibuka Secara Tersirat.

Pilih storan berdasarkan masa kandungan perlu dihasilkan

Contoh ini memanggil makeContent() semasa setiap pemulaan TileCard dan menyimpan nilai paparan yang dikembalikan. Ia tidak mewujudkan cache kekal merentas pemulaan komponen yang berikutnya. Mengekalkan kedua-dua sifat menunjukkan hubungan jenis yang diminta, tetapi komponen yang hanya memerlukan paparan terhasil boleh menggugurkan pembekal tersimpan.

Jika komponen patut meminta kandungan setiap kali body dinilainya, kekalkan pembekal sahaja. Berikut ialah pengisytiharan komponen pengganti untuk digunakan bersama protokol dan aplikasi di atas:

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider

    init(provider: Provider) {
        self.provider = provider
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            provider.makeContent()
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

Kedua-dua reka bentuk mengekalkan jenis berkaitan itu. Perbezaannya berpunca terus daripada tempat panggilan kaedah dibuat: semasa pemulaan dalam reka bentuk pertama, dan semasa penilaian body dalam reka bentuk kedua. Pilih masa itu dengan teliti, dan pastikan penghasilan kandungan tidak bergantung pada kaedah yang dipanggil tepat sekali.

Gunakan panduan keputusan ini apabila menyesuaikan contoh:

Keperluan komponen Strategi pengisytiharan Hubungan jenis
Simpan pembekal dan paparan yang dihasilkannya Pembekal generik bersama Provider.Content Mengekalkan identiti pembekal dan jenis kandungannya
Hasilkan kandungan semasa penilaian body Simpan pembekal generik Mengekalkan jenis berkaitan tanpa menyimpan nilai kandungan berasingan
Simpan pembekal dengan jenis kandungan sepunya yang diketahui Pertimbangkan eksistensial terkekang Mengekalkan kesamaan kandungan yang ditetapkan sambil menyembunyikan identiti pembekal
Sengaja padamkan jenis pelaksanaan paparan yang berbeza Gunakan AnyView pada sempadan yang dipilih Menyimpan jenis pembalut dan bukannya mendedahkan jenis paparan asal

Adakah jenis berkaitan utama menghapuskan keperluan untuk generik?

Jenis berkaitan utama menawarkan cara yang lebih ringkas untuk menyatakan kekangan jenis sama yang dipilih. Jika anda memiliki protokol itu, pengepala alternatif ini menjadikan Content sebagai jenis berkaitan utama; keperluan protokol yang lain kekal sama:

// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

Dengan pengisytiharan itu, any TileProvider<Text> menerangkan eksistensial yang jenis kandungan berkaitannya diketahui sebagai Text. Pelaksanaan pembekal yang berbeza hanya boleh berkongsi jenis storan itu jika pematuhan mereka mendedahkan kesamaan kandungan yang tepat tersebut. Sintaks ini mengehadkan jenis berkaitan; ia tidak menjadikan protokol itu struct generik. Lihat SE-0346 dan SE-0353.

Ini tidak menggabungkan jenis paparan sewenang-wenangnya yang tidak diketahui menjadi satu jenis konkrit yang dikekalkan. Khususnya, CircleTile mendedahkan hasil kandungan legap, jadi pemanggil tidak boleh menganggap hasil itu sebagai Text begitu sahaja. Gunakan komponen generik asal apabila kandungan berkaitan pembekal patut kekal tanpa sekatan.

Eksistensial terkekang dilaksanakan dalam Swift 5.7, dan SE-0353 secara jelas menyatakan had masa jalan dan penggunaan pada OS terdahulu. Oleh itu, sokongan pengkompil sahaja tidak mencukupi untuk memastikan keserasian penggunaan bagi reka bentuk pilihan itu. Semak diagnostik ketersediaan toolchain sasaran sebelum menggunakannya. Contoh utama tidak bergantung pada storan eksistensial terkekang. Lihat perbincangan ABI SE-0353.

Apakah yang perlu diperiksa apabila kompilasi gagal?

Jejaki jenis daripada keperluan protokol hingga ke sifat tersimpan sebelum mengubah reka letak:

  1. Pastikan protokol mengisytiharkan jenis berkaitan yang dikekang kepada View.
  2. Pastikan komponen mempunyai parameter pembekal generik bernama yang dikekang kepada protokol itu.
  3. Isytiharkan kandungan tersimpan sebagai Provider.Content, sepadan dengan jenis pulangan kaedah.
  4. Periksa sama ada induk telah menukar pembekal kepada any TileProvider.
  5. Jika menambah pengubah sebelum penyimpanan, periksa jenis hasilnya: paparan yang diubah suai mungkin bukan lagi Provider.Content.
  6. Periksa diagnostik pengasingan aktor secara berasingan daripada diagnostik jenis berkaitan.

Apple mendokumenkan bahawa pengubah paparan membalut paparan dengan paparan lain. Oleh itu, menetapkan provider.makeContent().padding() kepada sifat yang diisytiharkan sebagai Provider.Content mengubah kontrak yang perlu dipenuhi oleh penetapan itu. Simpan kandungan asal dan gunakan pengubah persembahan dalam body, atau reka semula jenis tersimpan supaya menerangkan hasil yang diubah suai. Lihat View.

Pembina juga mempunyai sempadan versi. Dokumentasi semasa Apple menerangkan tingkah laku ViewBuilder untuk Xcode 26 dan lebih awal, kemudian tingkah laku pembinaan kandungan yang berbeza bermula dengan Xcode 27. Contoh ini mengelakkan keperluan pembina tersuai: setiap pembekal mengembalikan satu ungkapan yang jelas. Jika anda mengembangkannya dengan API pembekal dipacu pembina, ikuti dokumentasi bagi versi Xcode yang benar-benar anda gunakan untuk membina. Lihat ViewBuilder.

Jika anda sengaja memilih AnyView, ambil kira tingkah laku yang didokumenkan: menukar jenis paparan yang dibalut akan memusnahkan hierarki lama dan mencipta hierarki baharu. Itu ialah keputusan reka bentuk yang berasingan daripada mengekalkan jenis berkaitan dalam storan. Apple mendokumenkan hal ini dalam AnyView.

Bagi komponen ini, kekalkan hubungan itu pada sempadan storannya: Provider generik, hasil bertipe Provider.Content, dan sifat tersimpan dengan jenis yang sama. Dengan itu, anda boleh mengubah reka letak dalaman pembekal sambil membiarkan pengkompil membawa jenis paparannya yang berkaitan merentasi komponen.

Sumber