Untuk tetingkap macOS yang memaparkan komponen SwiftUI yang heterogen, kekalkan output setiap pembekal dalam bentuk bertip kuat dan padamkan jenisnya apabila antara muka pemaparan bersama memerlukan satu jenis paparan konkrit. Gunakan pemadaman jenis pembekal lebih awal apabila koleksi itu sendiri memerlukan pembungkus seragam.

Artikel ini membina galeri komponen kecil yang mengandungi teks sintetik dan petak warna biru. Tugasnya ialah menyimpan pelaksanaan pembekal yang berbeza bersama-sama dan memaparkannya melalui satu antara muka SwiftUI. Keputusan reka bentuknya ialah menentukan sejauh mana hubungan antara pembekal dengan outputnya perlu didedahkan.

Apakah yang perlu dikekalkan oleh kontrak pembekal?

Pembekal dengan jenis output bersekutu menyatakan bahawa setiap jenis yang mematuhinya memilih jenis paparannya sendiri. Pelaksanaan konkrit boleh menyembunyikan pilihan itu di sebalik some View, sementara pengguna generik mengekalkan hubungan antara pembekal dengan outputnya. Jenis legap Swift mengekalkan identiti jenis; jenis protokol berkotak membolehkan jenis asas yang berbeza berkongsi satu antara muka. Lihat Jenis Protokol Legap dan Berkotak.

Berikut ialah cebisan kontrak, bukannya aplikasi lengkap:

import SwiftUI

@MainActor
protocol ViewProvider {
    associatedtype Content: View
    var id: String { get }
    func makeView() -> Content
}

@MainActor
struct TitleProvider: ViewProvider {
    let id: String
    let title: String

    func makeView() -> some View {
        Text(title).font(.headline)
    }
}

Di sini, TitleProvider.Content disimpulkan daripada jenis pulangan legap makeView(). Protokol memerlukan jenis bersekutu, dan pelaksanaan membekalkannya melalui inferens. SE-0244 mendokumenkan corak yang disokong ini secara jelas serta sekatan terhadap pengisytiharan jenis pulangan legap secara langsung sebagai keperluan protokol. Oleh itu, menggantikan keperluan itu dengan func makeView() -> some View bukan kontrak yang disokong seperti yang dihuraikan dalam cadangan tersebut. Lihat Jenis Hasil Legap.

Tatasusunan any ViewProvider menyelesaikan penyimpanan heterogen. SE-0309 dalam Swift 5.7 membenarkan penggunaan protokol dengan jenis bersekutu sebagai eksistensial, sambil mengekalkan sekatan pada operasi ahli tertentu. Membaca output melalui eksistensial itu boleh memadamkan jenis bersekutunya menjadi had atas protokol—dalam kes ini, any View. Lihat Membuka eksistensial untuk semua protokol.

Ini memberikan anda dua keputusan berasingan: cara menyimpan pembekal bercampur dan jenis konkrit yang dipulangkan oleh pemapar. Apple mendokumenkan AnyView sebagai paparan konkrit yang jenisnya dipadamkan dan mematuhi View, menjadikannya pilihan yang jelas untuk keputusan kedua. Lihat AnyView.

Pilih sempadan berdasarkan keperluan pengguna

Gunakan jadual keputusan ini sebelum mengubah protokol. Saranan ini menerapkan kontrak jenis yang didokumenkan pada tugasan galeri; ini panduan reka bentuk, bukannya hasil prestasi yang diukur.

Keperluan pengguna Reka bentuk yang sesuai Tempat maklumat jenis disembunyikan
Paparkan satu pembekal konkrit melalui kod yang boleh digunakan semula Pengguna generik yang dikekang kepada ViewProvider Kekalkan hubungan pembekal/output
Simpan pembekal yang berbeza dan kekalkan metadata bersamanya [any ViewProvider], dengan pembantu pemaparan yang memulangkan AnyView Jenis pembekal semasa penyimpanan; jenis output semasa pemaparan
Dedahkan satu pembungkus pembekal konkrit kepada kod hiliran [AnyViewProvider] Semasa pembungkus dibina
Wajibkan setiap pelaksanaan memulangkan paparan yang jenisnya sudah dipadamkan Keperluan protokol yang memulangkan AnyView Dalam setiap pelaksanaan
Paparkan set alternatif komponen yang kecil dan tetap Enum dan switch berasaskan pembina Alternatif kekal dinyatakan secara jelas dalam enum

Untuk galeri ini, pilih penyimpanan eksistensial serta pemadaman jenis output. Pembekal mengekalkan jenis output masing-masing, koleksi mendedahkan pengecam rentetan yang stabil, dan satu pembantu mengendalikan penukaran kepada AnyView. Perubahan pada reka letak dalaman komponen tidak lagi memerlukan kod pemadaman jenis baharu dalam komponen itu.

Parameter koleksi generik sesuai untuk tugasan yang berbeza. Tatasusunan yang jenis elemennya ialah satu jenis pembekal generik tidak dapat mewakili beberapa jenis pembekal yang tidak berkaitan secara automatik. Pembukaan eksistensial tersirat Swift berfungsi pada satu argumen eksistensial; ia tidak memberikan tatasusunan heterogen satu jenis asas pematuh yang dikongsi. Lihat Eksistensial yang Dibuka Secara Tersirat.

Bina galeri komponen macOS yang lengkap

Contoh ini memerlukan pengkompil Swift 5.7 atau lebih baharu dan sasaran aplikasi SwiftUI macOS. Tetapkan sasaran penggunaan minimum sampel kepada macOS 12 atau lebih baharu. Dalam projek Aplikasi macOS Xcode yang menggunakan SwiftUI, gantikan pengisytiharan aplikasi yang dijana dengan atur cara lengkap di bawah, dan kekalkan tepat satu pengisytiharan @main.

Anotasi @MainActor yang jelas memastikan penciptaan dan pemaparan pembekal berlaku pada aktor yang sama. Pengisytiharan View Apple yang terkini diasingkan kepada aktor utama. Dokumentasinya turut menerangkan bahawa paparan tersuai membekalkan kandungannya melalui body. Lihat View.

import SwiftUI

@MainActor
protocol ViewProvider {
    associatedtype Content: View
    var id: String { get }
    func makeView() -> Content
}

@MainActor
struct TitleProvider: ViewProvider {
    let id: String
    let title: String

    func makeView() -> some View {
        Text(title)
            .font(.headline)
    }
}

@MainActor
struct SwatchProvider: ViewProvider {
    let id: String

    func makeView() -> some View {
        RoundedRectangle(cornerRadius: 8)
            .fill(Color.blue)
            .frame(width: 96, height: 48)
    }
}

@MainActor
func erasedView<P: ViewProvider>(for provider: P) -> AnyView {
    AnyView(provider.makeView())
}

@MainActor
struct GalleryView: View {
    let providers: [any ViewProvider]

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            ForEach(providers, id: \.id) { provider in
                erasedView(for: provider)
            }
        }
        .padding(24)
        .frame(minWidth: 280, minHeight: 180)
    }
}

@main
@MainActor
struct ProviderGalleryApp: App {
    private let providers: [any ViewProvider] = [
        TitleProvider(id: "title", title: "Component gallery"),
        SwatchProvider(id: "swatch")
    ]

    var body: some Scene {
        WindowGroup {
            GalleryView(providers: providers)
        }
    }
}

Hasil yang dijangka ialah satu tetingkap yang memaparkan Component gallery di atas segi empat tepat biru bucu bulat. Semua input ialah nilai sintetik setempat. Contoh ini telah diperiksa tetapi tidak dikompil di sini kerana persekitaran yang tersedia tidak mempunyai pengkompil Swift atau SDK Apple.

Hasil pembantu itu sentiasa AnyView, tanpa mengira jenis output bersekutu pembekal. Dalam pembantu itu, pembekal adalah generik dan hubungan Content-nya kekal tersedia. Apple mendokumenkan pemula yang digunakan di sini sebagai menerima nilai generik yang dikekang kepada View lalu memadamkan jenis nilai itu. Lihat AnyView.init(_:).

Koleksi menggunakan id: \.id kerana protokol menetapkan id kepada String; protokol itu tidak memerlukan pemulihan jenis bersekutu lain yang tidak diketahui. Dokumentasi ForEach Apple membenarkan elemen yang boleh dikenal pasti atau laluan kunci pengecam yang dinyatakan secara jelas. Pastikan pengecam unik dalam koleksi ini dan stabil untuk komponen logik yang sama merentas kemas kini. Lihat ForEach.

Mengapa pembantu generik boleh berfungsi sedangkan pembungkus paparan generik gagal?

Sifat penting erasedView ialah jenis pulangannya yang konkrit. Swift 5.7 memperkenalkan pembukaan argumen eksistensial secara tersirat, yang membolehkan pengkompil memautkan parameter generik kepada jenis asas nilai eksistensial semasa panggilan. Namun, sekatan masih dikenakan pada hasil yang akan mendedahkan jenis terbuka itu melalui bekas generik invarian. SE-0352 turut merekodkan tingkah laku pembukaan yang lebih luas dalam mod bahasa Swift 6. Lihat Eksistensial yang Dibuka Secara Tersirat.

Sebagai contoh, cebisan diagnostik berikut menambah pembungkus generik pada atur cara di atas. Panggilan yang dikomen menunjukkan sempadan yang bermasalah; ia bukan titik masuk aplikasi tambahan:

@MainActor
struct ProviderRow<P: ViewProvider>: View {
    let provider: P

    var body: some View {
        provider.makeView()
    }
}

@MainActor
func inspectBoundary(_ provider: any ViewProvider) {
    // Problematic: the constructed result would retain the opened P.
    // let row = ProviderRow(provider: provider)

    // Supported boundary: the result is the concrete type AnyView.
    let row = erasedView(for: provider)
    _ = row
}

Ini menjelaskan sebab penambahan lapisan generik sahaja mungkin tidak menyelesaikan ralat itu. Periksa kedua-dua parameternya dan hasilnya. Pembantu yang memulangkan paparan konkrit yang jenisnya dipadamkan menyelesaikan penukaran dalam panggilan tersebut; hasil seperti ProviderRow<P> mengekalkan jenis pembekal dalam strukturnya.

Begitu juga, some View bukan cara menulis nilai pulangan yang jenis asasnya boleh berubah sesuka hati semasa masa jalan. Pelaksanaan memilih jenis asas yang konsisten, yang mungkin bergantung pada argumen generik. Pembina boleh mengekodkan cabang ke dalam hasil gabungan, tetapi pengisytiharan hasil legap semata-mata tidak menyatukan jenis pulangan yang tidak berkaitan. Lihat Jenis Protokol Legap dan Berkotak.

Bilakah anda patut memadamkan jenis pembekal itu sendiri?

Pilih pembungkus konkrit apabila antara muka hiliran patut menerima satu jenis pembekal, atau apabila anda mahu pemanggil menyeragamkan pembekal sebelum menyimpannya. Pengisytiharan tambahan berikut berfungsi dengan protokol dan pembekal sedia ada dalam sampel. Ia menangkap pembekal dan mencipta paparannya apabila makeView() dipanggil.

@MainActor
struct AnyViewProvider: ViewProvider {
    let id: String
    private let build: @MainActor () -> AnyView

    init<P: ViewProvider>(_ provider: P) {
        id = provider.id
        build = { AnyView(provider.makeView()) }
    }

    func makeView() -> AnyView {
        build()
    }
}

Untuk menggunakan alternatif ini, ubah kedua-dua sifat providers yang disimpan dalam atur cara lengkap kepada [AnyViewProvider]. Gantikan tatasusunan awal aplikasi dengan ungkapan berikut:

[
    AnyViewProvider(
        TitleProvider(id: "title", title: "Component gallery")
    ),
    AnyViewProvider(SwatchProvider(id: "swatch"))
]

Kemudian gantikan erasedView(for: provider) dalam penutupan ForEach galeri dengan provider.makeView(). Pembungkus itu sudah memulangkan AnyView, jadi panggilan tersebut tidak memerlukan penukaran tambahan. Pelaksanaan ini menyalin pengecam dan menangkap nilai pembekal yang dibekalkan; ia bukan mekanisme untuk menggantikan nilai yang ditangkap itu kemudian.

Jika setiap pengguna memerlukan output yang jenisnya dipadamkan, protokol yang lebih ringkas boleh mewajibkan makeView() -> AnyView secara langsung. Ini mengurangkan kod penyesuai tetapi meletakkan tanggungjawab pemadaman jenis pada setiap pematuh. Pilihnya apabila output yang jenisnya dipadamkan merupakan janji antara muka yang disengajakan. Pilih kontrak jenis bersekutu apabila pengguna generik masih patut dapat menggunakan hubungan output setiap pembekal.

Kekalkan batasan pengkompil dan identiti paparan

Asingkan ciri bahasa daripada tingkah laku pembina. Sampel ini bergantung pada ciri eksistensial Swift 5.7 dan hasil AnyView yang dinyatakan secara jelas. Dokumentasi Apple yang terkini membezakan Xcode 26 dan yang lebih awal daripada Xcode 27 dan yang lebih baharu: versi yang lebih baharu membina kandungan pembina yang bebas jenis dan mengesyorkan ContentBuilder untuk tingkah laku itu. Dokumentasinya masih menerangkan keselamatan jenis melalui pematuhan bersyarat. Jangan tafsirkan kelonggaran semakan input pembina sebagai jaminan umum bahawa eksistensial itu sendiri memenuhi setiap kekangan generik View. Lihat ViewBuilder dan ContentBuilder.

Untuk set komponen yang tetap, pertimbangkan enum yang kes-kesnya membawa data komponen dan sifat pemaparannya melakukan pertukaran berdasarkan kes tersebut. Ini menjadikan alternatif yang disokong dinyatakan secara jelas. Reka bentuk ini berguna apabila penambahan komponen baharu sepatutnya memerlukan kemas kini pada satu pemapar pusat; pendekatan pembekal sesuai untuk antara muka yang pelaksanaan pematuhnya sendiri membekalkan pemaparan.

Pemadaman jenis juga mempunyai kesan terhadap identiti yang didokumenkan. Apple menyatakan bahawa apabila jenis yang dibungkus oleh AnyView berubah, hierarki terdahulu dimusnahkan dan hierarki baharu dicipta. Pengecam koleksi yang stabil tidak mengatasi tingkah laku itu. Jika komponen boleh bertukar antara jenis paparan terbungkus yang berbeza, semak keadaan dan tingkah laku peralihannya dalam aplikasi sebenar. Lihat AnyView.

Untuk galeri ini, kekalkan kontrak pembekal jenis bersekutu, simpan nilai bercampur sebagai eksistensial, dan pulangkan AnyView daripada satu pembantu pemaparan. Alihkan pemadaman jenis ke dalam pembungkus pembekal konkrit apabila antara muka hiliran memerlukan jenis seragam itu. Ini memberikan sempadan yang jelas untuk diperiksa setiap kali koleksi, kontrak pemaparan atau konfigurasi pengkompil berubah.

Sumber