Per costruire un componente SwiftUI generico, richiedi il suo inizializzatore in un protocollo oppure inietta una closure che lo restituisca. Questo tutorial usa entrambi gli approcci per visualizzare lo stesso elemento sintetico in una finestra macOS, specificando esplicitamente i limiti del compilatore e dell’SDK.

Che cosa deve garantire un componente generico?

Supponiamo che un’app macOS visualizzi una piccola scheda elemento. Vuoi un pannello riutilizzabile che costruisca la scheda a partire da un elemento, consentendo al contempo ai chiamanti di scegliere la presentazione. La domanda centrale è chi gestisce la costruzione: l’interfaccia dichiarata del componente oppure il chiamante che fornisce il componente.

Un vincolo generico dà all’implementazione accesso alle funzionalità indicate da quel vincolo. Scrivere Component: View stabilisce che il componente è una vista SwiftUI; non stabilisce che il componente abbia un inizializzatore chiamato init(item:). Il capitolo sui generics di Swift spiega i vincoli sui protocolli, mentre la documentazione di View di Apple descrive la proprietà body richiesta dalla vista.

Di conseguenza, questo frammento è intenzionalmente non valido:

// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
    Component(item: item)
}

Aggiungere quell’inizializzatore a un singolo componente concreto non lo rende disponibile tramite ogni parametro generico vincolato soltanto a View. L’API riutilizzabile ha bisogno di un contratto di costruzione esplicito. Prima di modificare il componente, usa questa tabella decisionale:

Requisito di costruzione Interfaccia adatta Motivo
Ogni componente partecipante accetta lo stesso elemento Protocollo che richiede init(item:) La costruzione fa parte dell’interfaccia della famiglia di componenti.
I chiamanti scelgono configurazioni aggiuntive o modificatori Closure factory che restituisce il componente generico La costruzione resta nel punto di chiamata.
Il chiamante ha già un componente pronto Memorizzare il valore del componente generico La costruzione è già avvenuta.
La presentazione alterna un insieme finito di layout Comporre le alternative all’interno di una vista SwiftUI L’API circostante può mantenere una struttura concreta della vista.

Questa tabella è un supporto alla progettazione ricavato dalle regole del linguaggio documentate. Le prime due opzioni sono implementate insieme qui sotto, così da poterne confrontare direttamente il comportamento.

Crea un esempio macOS completo

Usa un target app macOS di Xcode con SwiftUI e una versione minima di macOS 11 o successiva. L’esempio è scritto per un compilatore Swift 6 con il corrispondente SDK Apple. Le annotazioni esplicite @MainActor richiedono Swift 5.5 o successivo, quindi il codice non è pensato per compilatori precedenti. Apple documenta WindowGroup come disponibile da macOS 11 e come contenitore per la gerarchia di viste di un’app. Vedi WindowGroup.

Sostituisci il file generato per il punto d’ingresso dell’app con il programma completo seguente. Assicurati che il target contenga una sola dichiarazione @main dell’app. Il file generato ContentView può restare inutilizzato. Non servono pacchetti, risorse esterne o dati di esempio scaricati.

import SwiftUI

struct Item {
    let title: String
    let detail: String
}

@MainActor
protocol ItemComponent: View {
    init(item: Item)
}

@MainActor
struct ItemCard: ItemComponent {
    let item: Item

    init(item: Item) {
        self.item = item
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(item.title)
                .font(.headline)
            Text(item.detail)
        }
        .padding()
        .background(Color.gray.opacity(0.12))
        .cornerRadius(8)
    }
}

@MainActor
struct ContractPanel<Component: ItemComponent>: View {
    let item: Item

    init(item: Item, component: Component.Type) {
        self.item = item
    }

    var body: some View {
        Component(item: item)
    }
}

@MainActor
struct FactoryPanel<Component: View>: View {
    private let makeComponent: @MainActor () -> Component

    init(make: @escaping @MainActor () -> Component) {
        self.makeComponent = make
    }

    var body: some View {
        makeComponent()
    }
}

@MainActor
struct DemoView: View {
    private let item = Item(
        title: "Sample tile",
        detail: "Blue circle on a neutral background"
    )

    var body: some View {
        VStack(alignment: .leading, spacing: 18) {
            Text("Initializer contract")
            ContractPanel(item: item, component: ItemCard.self)

            Text("Injected factory")
            FactoryPanel {
                ItemCard(item: item)
                    .padding(.leading, 12)
            }

            Text("Alternative factory output")
            FactoryPanel {
                Text(item.title + ": " + item.detail)
            }
        }
        .padding(24)
        .frame(width: 420)
    }
}

@main
@MainActor
struct ComponentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            DemoView()
        }
    }
}

Esegui il target app macOS. Il risultato previsto è una finestra con due presentazioni della scheda e una presentazione in testo semplice. La seconda scheda ha una spaziatura iniziale aggiuntiva fornita dal chiamante. Sono esempi sintetici del comportamento dell’API, non risultati misurati. Nell’ambiente di ricerca non è installato alcun compilatore Swift, quindi il codice è stato esaminato ma non compilato né eseguito.

Le annotazioni dell’attore principale rendono esplicito il confine di costruzione dell’interfaccia utente. La dichiarazione View attuale di Apple è annotata con @MainActor e @preconcurrency; la relativa documentazione spiega anche come si eredita l’isolamento quando la conformità compare nella dichiarazione originale di un tipo. Vedi View.

Come funziona il contratto dell’inizializzatore?

ItemComponent estende View e aggiunge esattamente un requisito di costruzione: init(item:). Swift supporta direttamente i requisiti degli inizializzatori nei protocolli. Un tipo conforme fornisce l’implementazione; il protocollo ne dichiara la firma senza un corpo dell’inizializzatore. Vedi Protocolli: requisiti degli inizializzatori.

Questo requisito rende valido Component(item: item) all’interno di ContractPanel. L’argomento component: Component.Type fornisce un tipo concreto per l’inferenza generica: passando ItemCard.self, si determina che il tipo di componente di questo pannello è ItemCard. Il pannello non deve memorizzare il metatipo, perché il suo body costruisce tramite il parametro di tipo generico.

Questo contratto è utile quando ogni componente deve poter essere costruito a partire dallo stesso input. Per esempio, una famiglia di presentazioni compatte ed espanse di elementi, gestita dall’app, potrebbe condividere questo requisito. Il chiamante può quindi cambiare il tipo di componente selezionato senza riscrivere l’espressione di costruzione del pannello.

Il compromesso emerge quando un’implementazione ha bisogno di configurazioni aggiuntive. Richiedere init(item:) significa che ogni componente conforme deve offrire quel percorso di costruzione, anche se il suo inizializzatore preferito accetta anche una didascalia o un’impostazione di layout. Se queste informazioni aggiuntive spettano al chiamante, l’approccio factory qui sotto di solito esprime il requisito in modo più diretto.

Per le implementazioni basate su classi, un inizializzatore che soddisfa un requisito del protocollo deve essere required, a meno che la classe non sia final. Una sottoclasse che esegue anche l’override di un inizializzatore designato corrispondente della superclasse potrebbe aver bisogno sia di required sia di override. Le struct come ItemCard non usano required. Queste distinzioni sono documentate in Protocolli.

Quando conviene spostare la costruzione in una factory?

FactoryPanel accetta una funzione di tipo @MainActor () -> Component. La sua unica garanzia è che chiamando la funzione si ottiene il tipo di componente selezionato. La closure può invocare un inizializzatore, catturare una configurazione oppure restituire una vista con modificatori applicati.

Il programma mostra la differenza pratica: la factory può restituire un ItemCard con spaziatura interna oppure un Text, senza richiedere che nessuno dei due tipi risultanti adotti ItemComponent. Apple spiega che i modificatori delle viste possono racchiudere l’input in un’altra vista; per questo è utile lasciare che il compilatore inferisca qui il tipo risultante della factory. Vedi View.

L’inizializzatore contrassegna la closure con @escaping perché il pannello la memorizza per invocarla in seguito. Il capitolo sulle closure di Swift documenta le closure escaping e la loro capacità di catturare valori circostanti. In questo esempio, la closure cattura l’elemento sintetico usato dalla vista circostante.

Scegli con attenzione gli input della factory. Una factory senza argomenti significa che il chiamante cattura tutto ciò che serve per la costruzione. Se è il pannello a selezionare un elemento, una factory parametrizzata può esprimere più chiaramente questa responsabilità. La definizione seguente è una sostituzione della definizione del componente da usare insieme all’Item esistente; non è una seconda app completa:

@MainActor
struct ItemFactoryPanel<Component: View>: View {
    let item: Item
    private let makeComponent: @MainActor (Item) -> Component

    init(
        item: Item,
        make: @escaping @MainActor (Item) -> Component
    ) {
        self.item = item
        self.makeComponent = make
    }

    var body: some View {
        makeComponent(item)
    }
}

Con questa definizione, il pannello fornisce l’elemento e il chiamante fornisce la presentazione. Limita il lavoro della factory alla produzione del valore della vista: il contratto di costruzione dell’esempio non promette un’esecuzione una tantum, una memorizzazione persistente o la memorizzazione nella cache.

Le factory hanno bisogno di un result builder?

Le factory del programma completo sono closure ordinarie. Ognuna restituisce una singola espressione, quindi non serve un’annotazione builder personalizzata. Se sono necessarie più viste figlie, per questa API basta restituire un singolo VStack; lo stack contiene le viste figlie.

Un result builder è utile quando il parametro factory stesso dovrebbe accettare una sequenza dichiarativa di espressioni o contenuti condizionali. Swift Evolution registra l’introduzione standardizzata dei result builder come implementata in Swift 5.4. Vedi SE-0289: Result builder.

Quando adotti la sintassi builder, rispetta i limiti dell’SDK. La documentazione attuale di Apple descrive @ViewBuilder per i parametri che producono viste in Xcode 26 e versioni precedenti. Per Xcode 27 e versioni successive, descrive la costruzione agnostica rispetto al tipo e consiglia l’alias ContentBuilder per indicare tale comportamento. Vedi ViewBuilder e ContentBuilder. La closure ordinaria usata qui evita di richiedere questo alias più recente.

Perché i tipi opachi o esistenziali non sostituiscono il contratto?

some View nasconde uno specifico tipo sottostante preservandone l’identità. Funziona bene per le proprietà body di questo programma. Non aggiunge init(item:) all’interfaccia View. Un valore in box scritto come any View nasconde il tipo sottostante in modo diverso e non ne preserva la stessa identità statica del tipo. Vedi Tipi di protocollo opachi e in box.

Non trasformare questo punto nell’affermazione generalizzata che i valori esistenziali non possano mai essere passati a funzioni generiche. SE-0352 ha introdotto gli esistenziali aperti implicitamente in Swift 5.7, consentendo alle chiamate idonee di associare un parametro generico al tipo sottostante di un esistenziale. Documenta anche alcune restrizioni, compresi i casi in cui il risultato incorpora il tipo aperto in un wrapper generico, rendendo impossibile rappresentare il risultato dopo l’eliminazione del tipo. Vedi Esistenziali aperti implicitamente.

Per questo caso d’uso del pannello, è sufficiente mantenere il tipo concreto del componente tramite un parametro generico. Non occorre introdurre una memorizzazione esistenziale solo per consentire ai chiamanti di scegliere come costruire una vista.

Come diagnosticare un errore di costruzione?

Segui l’espressione che causa l’errore e controlla il contratto disponibile in quel punto:

  1. Se Component(item:) causa un errore, verifica che il vincolo generico includa un protocollo che richiede quella firma esatta dell’inizializzatore.
  2. Se il compilatore non riesce a inferire Component, specifica un metatipo concreto come ItemCard.self oppure una factory con un’espressione di ritorno concreta.
  3. Se un componente ha bisogno di una configurazione specifica del chiamante, sposta la costruzione nella factory invece di aggiungere al protocollo requisiti non pertinenti.
  4. Se la memorizzazione di una closure genera una diagnostica relativa a una closure escaping, controlla l’annotazione @escaping dell’inizializzatore.
  5. Se la costruzione genera una diagnostica di isolamento dell’attore, verifica che sia il punto di chiamata sia il tipo di funzione della factory esprimano il confine previsto dell’attore principale.
  6. Se un esistenziale causa un errore relativo al tipo generico del risultato, consulta prima le restrizioni sul tipo del risultato in SE-0352, invece di presumere che un aggiornamento del compilatore rimuova il limite.

Per una famiglia di componenti macOS condivisa, richiedi l’inizializzatore quando la costruzione uniforme fa parte dell’interfaccia. Usa una factory iniettata quando il chiamante deve assemblare e configurare la presentazione. Entrambi i percorsi rendono esplicita la promessa di costruzione mancante, preservando il tipo concreto del componente necessario al pannello.

Fonti