Rendi il componente SwiftUI generico sul tipo che conforma al protocollo, quindi dichiara la vista memorizzata come Provider.Content. In questo modo, la relazione tra il provider e il suo tipo di vista associato si mantiene in tutto il componente.
In questo tutorial realizzerai una piccola finestra macOS che contiene due riquadri di esempio. L’obiettivo è memorizzare un provider e la vista che produce senza sostituire il tipo associato con any View o AnyView. Lo stesso schema si applica quando un componente desktop riutilizzabile accetta un’etichetta personalizzata, un elemento accessorio o un provider di contenuti.
Quale relazione devono preservare le proprietà memorizzate?
Un tipo associato indica un tipo scelto da ciascuna conformità a un protocollo. Se un protocollo dichiara associatedtype Content: View, il provider che vi conforma deve fornire uno specifico tipo di contenuto. Il codice generico può fare riferimento a quel tipo come Provider.Content, anche quando l’implementazione è nascosta dietro un risultato opaco. Swift documenta i tipi associati e i vincoli generici nel capitolo sui generics.
Il componente ha quindi bisogno di due dichiarazioni collegate: un parametro generico vincolato al protocollo del provider e una proprietà di contenuto memorizzata il cui tipo sia il tipo associato di quel parametro. Dichiarare due proprietà indipendenti che si limitano a conformare a dei protocolli non esprime questo collegamento.
SwiftUI usa al suo interno un contratto correlato: View dichiara associatedtype Body: View e Swift deduce quel tipo associato dall’implementazione di body di una vista personalizzata. Content del provider e Body del componente sono tipi associati distinti e non devono coincidere. Consulta la documentazione di Body di Apple.
Crea un esempio macOS completo
Per questo esempio, usa un progetto app macOS di Xcode con SwiftUI e una versione di macOS 13 o successive come destinazione di distribuzione. Sostituisci le dichiarazioni generate per l’app e la vista dei contenuti con il seguente singolo file, mantenendo una sola dichiarazione @main nel target. Apple documenta questa struttura di ingresso dell’app e l’uso di WindowGroup in App.
L’esempio usa volutamente la sintassi tradizionale dei tipi associati e dei generics con nome. I risultati opachi sono stati introdotti in Swift 5.1; la sintassi facoltativa dei tipi associati primari, discussa più avanti, richiede Swift 5.7. Il file completo è pensato per una toolchain Swift 6 attuale. È stato verificato rispetto alla documentazione consultata, ma qui non è stato compilato perché questo ambiente non dispone di un compilatore Swift. La versione macOS scelta come destinazione di distribuzione è un prerequisito dell’esempio, non un’indicazione della versione minima del sistema operativo che supporta tutte le funzionalità sottostanti. Consulta SE-0244 e 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()
}
}
}
Compila ed esegui il target macOS. Il risultato previsto è una finestra con un riquadro di testo e un riquadro con un cerchio blu accanto alla didascalia. Entrambi usano lo stesso componente generico, mentre il tipo del provider viene dedotto indipendentemente in ciascun punto di chiamata.
Le annotazioni esplicite @MainActor collocano queste dichiarazioni che producono UI nello stesso contesto di attore. L’attuale documentazione di View di Apple dichiara il protocollo con isolamento main actor. Qui le annotazioni rendono esplicito anche il contesto previsto per il protocollo personalizzato del provider.
Perché il componente generico preserva il tipo?
Per la prima scheda, Provider è TextTile, quindi Provider.Content è Text. Per la seconda scheda, Provider è CircleTile e il relativo tipo di contenuto associato è il risultato opaco di CircleTile.makeContent(). Puoi indicare quel tipo tramite CircleTile.Content senza specificare il tipo annidato del layout SwiftUI.
L’assegnazione essenziale è self.content = provider.makeContent(). Il protocollo garantisce che questo metodo restituisca esattamente Provider.Content, cioè proprio il tipo dichiarato per la proprietà memorizzata. Si tratta di una relazione verificata in compilazione e derivata dal vincolo generico, non di un confronto tra tipi a runtime. Il capitolo sui generics di Swift spiega come accedere ai tipi associati tramite parametri generici vincolati.
L’uso di some View nell’implementazione conforme non cancella questa relazione. Un risultato opaco nasconde l’implementazione concreta preservandone l’identità di tipo. Per questo è adatto come tipo di riferimento per il tipo associato. Swift spiega la distinzione in Tipi di protocollo opachi e boxed.
Queste schede hanno specializzazioni diverse: TileCard<TextTile> e TileCard<CircleTile>. Riutilizzare la dichiarazione sorgente non richiede che tutte le schede abbiano un unico tipo di contenuto memorizzato universale.
Perché le proprietà opache o esistenziali indipendenti non risolvono il problema?
I seguenti frammenti illustrano errori di compilazione e non sostituiscono il programma completo:
// 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
Una proprietà memorizzata opaca richiede un inizializzatore nella dichiarazione che ne stabilisca il tipo sottostante. Scrivere some View non crea un parametro generico scelto dal chiamante che un inizializzatore arbitrario di componente possa valorizzare in seguito. Per esempio, let content: some View = Text("Sample") stabilisce un tipo per quella dichiarazione; non collega la proprietà al tipo associato di un’altra proprietà. Questa regola per le proprietà memorizzate è documentata in SE-0244: tipi di risultato opachi.
Un esistenziale come any TileProvider consente di memorizzare provider di diversi tipi conformi. Nasconde il tipo specifico che conforma, quindi la dichiarazione non espone più la relazione come Provider.Content. Allo stesso modo, any View è un contenitore esistenziale: non offre lo stesso contratto di un tipo generico con nome che conforma a View. Consulta la spiegazione di Swift sui tipi di protocollo boxed.
Swift può aprire implicitamente gli argomenti esistenziali per alcune chiamate generiche. Questo non rende ogni esistenziale utilizzabile in qualsiasi inizializzatore generico. In particolare, SE-0352 limita l’apertura quando il tipo sottostante sconosciuto fuoriesce all’interno di un risultato generico come X<T>. Una specializzazione di un componente generico può incontrare proprio questo limite. Mantieni il provider generico prima di creare la scheda, invece di presumere che un esistenziale possa sempre essere riconvertito in una specializzazione persistente. Consulta SE-0352: apertura implicita degli esistenziali.
Scegli lo storage in base a quando deve essere prodotto il contenuto
L’esempio chiama makeContent() a ogni inizializzazione di TileCard e memorizza il valore della vista restituito. Non crea una cache persistente per le successive inizializzazioni del componente. Mantenere entrambe le proprietà dimostra la relazione tra i tipi richiesta, ma un componente a cui serve solo la vista risultante può omettere il provider memorizzato.
Se il componente deve richiedere il contenuto ogni volta che viene valutato il suo body, conserva solo il provider. La seguente dichiarazione del componente sostituisce quella precedente e va usata con il protocollo e l’app descritti sopra:
@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))
}
}
Entrambe le soluzioni preservano il tipo associato. La differenza deriva direttamente dal punto in cui viene chiamato il metodo: durante l’inizializzazione nella prima soluzione e durante la valutazione del body nella seconda. Scegli consapevolmente questo momento e fai in modo che la produzione del contenuto non dipenda dal fatto che il metodo venga chiamato esattamente una volta.
Usa questa guida decisionale per adattare l’esempio:
| Requisito del componente | Strategia di dichiarazione | Relazione tra i tipi |
|---|---|---|
| Memorizzare il provider e la vista che produce | Provider generico più Provider.Content |
Preserva sia l’identità del provider sia il tipo di contenuto |
| Produrre contenuto durante la valutazione del body | Memorizzare il provider generico | Preserva il tipo associato senza memorizzare un valore di contenuto separato |
| Memorizzare provider con un tipo di contenuto comune noto | Valutare un esistenziale vincolato | Preserva l’uguaglianza specificata del tipo di contenuto e nasconde l’identità del provider |
| Cancellare deliberatamente implementazioni di viste diverse | Usare AnyView a un confine scelto |
Memorizza il tipo wrapper invece di esporre il tipo di vista originale |
I tipi associati primari eliminano la necessità dei generics?
I tipi associati primari offrono un modo più conciso per esprimere determinati vincoli di uguaglianza tra tipi. Se gestisci il protocollo, questa intestazione alternativa rende Content il tipo associato primario; gli altri requisiti del protocollo restano invariati:
// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
associatedtype Content: View
var title: String { get }
func makeContent() -> Content
}
Con questa dichiarazione, any TileProvider<Text> descrive un esistenziale il cui tipo di contenuto associato è noto: Text. Implementazioni diverse del provider possono condividere quel tipo di storage solo se le loro conformità espongono esattamente quell’uguaglianza del tipo di contenuto. La sintassi pone un vincolo su un tipo associato; non trasforma il protocollo in una struct generica. Consulta SE-0346 e SE-0353.
Questo non combina tipi di vista arbitrari e sconosciuti in un unico tipo concreto preservato. In particolare, CircleTile espone un risultato di contenuto opaco, quindi i chiamanti non possono semplicemente trattarlo come Text. Usa il componente generico originale quando il contenuto associato del provider deve restare senza vincoli.
Gli esistenziali vincolati sono stati introdotti in Swift 5.7 e SE-0353 indica esplicitamente i limiti a runtime e di distribuzione con versioni precedenti. Il solo supporto del compilatore non basta quindi a stabilire la compatibilità con la versione di distribuzione per questa soluzione facoltativa. Prima di adottarla, verifica gli avvisi di disponibilità della toolchain di destinazione. L’esempio principale non dipende dallo storage con esistenziali vincolati. Consulta la discussione ABI di SE-0353.
Cosa controllare se la compilazione fallisce?
Prima di modificare il layout, segui il tipo dal requisito del protocollo alla proprietà memorizzata:
- Verifica che il protocollo dichiari un tipo associato vincolato a
View. - Verifica che il componente abbia un parametro provider generico con nome, vincolato a quel protocollo.
- Dichiara il contenuto memorizzato come
Provider.Content, in modo che corrisponda al tipo restituito dal metodo. - Verifica se un componente genitore ha già convertito il provider in
any TileProvider. - Se aggiungi un modificatore prima dello storage, controllane il tipo restituito: la vista modificata potrebbe non essere più
Provider.Content. - Controlla separatamente gli avvisi relativi all’isolamento degli attori e quelli relativi ai tipi associati.
Apple documenta che i modificatori di vista racchiudono una vista in un’altra vista. Di conseguenza, assegnare provider.makeContent().padding() a una proprietà dichiarata come Provider.Content modifica il contratto che l’assegnazione deve rispettare. Memorizza il contenuto originale e applica i modificatori di presentazione in body, oppure riprogetta il tipo memorizzato affinché descriva il risultato modificato. Consulta View.
Anche i builder hanno limiti legati alla versione. L’attuale documentazione di Apple descrive il comportamento di ViewBuilder per Xcode 26 e versioni precedenti, quindi illustra un diverso comportamento nella costruzione dei contenuti a partire da Xcode 27. L’esempio evita un requisito personalizzato per i builder: ogni provider restituisce un’unica espressione esplicita. Se lo estendi con API per provider basate su builder, segui la documentazione della versione di Xcode con cui effettui la compilazione. Consulta ViewBuilder.
Se scegli deliberatamente AnyView, considera il comportamento documentato: quando cambia il tipo della vista racchiusa, la gerarchia precedente viene distrutta e ne viene creata una nuova. È una decisione progettuale distinta dalla preservazione di un tipo associato nello storage. Apple lo documenta in AnyView.
Per questo componente, mantieni visibile la relazione al confine dello storage: un Provider generico, un risultato di tipo Provider.Content e una proprietà memorizzata dello stesso tipo. Così puoi modificare il layout interno di un provider e lasciare che il compilatore propaghi il suo tipo di vista associato nel componente.
Fonti
- Generics — Swift.org
- Tipi di protocollo opachi e boxed — Swift.org
- View — Apple
- Body — Apple
- App — Apple
- ViewBuilder — Apple
- AnyView — Apple
- SE-0244: tipi di risultato opachi — Swift Evolution
- SE-0346: requisiti leggeri di uguaglianza tra tipi per i tipi associati primari — Swift Evolution
- SE-0352: apertura implicita degli esistenziali — Swift Evolution
- SE-0353: tipi esistenziali vincolati — Swift Evolution