Assegna un aspetto predefinito a un componente generico SwiftUI aggiungendo un inizializzatore vincolato a un tipo di contenuto concreto. Mantieni un inizializzatore separato basato su builder per la personalizzazione. Questa guida crea un pannello riutilizzabile per una finestra macOS e spiega quando l’inferenza degli argomenti predefiniti di Swift 5.7 può semplificare l’interfaccia.
Definisci le due chiamate che il componente deve supportare
Il compito per il lettore è semplice: creare un pannello desktop che appaia completo quando viene chiamato con solo un titolo, consentendo al contempo a un’altra schermata di fornire il proprio contenuto. Il titolo circostante, la spaziatura e il bordo devono rimanere coerenti in entrambi i casi.
L’interfaccia prevista è DesktopPanel(title: "Workspace") per la presentazione predefinita e DesktopPanel(title: "Workspace") { Text("Three sample items") } per il contenuto personalizzato. Queste chiamate creano specializzazioni diverse dello stesso componente generico.
I parametri generici di Swift rappresentano tipi concreti. Un vincolo come Content: View richiede la conformità; un requisito di uguaglianza di tipo come Content == DefaultPanelContent seleziona un tipo specifico. Il riferimento del linguaggio documenta entrambi questi requisiti e la risoluzione degli overload degli inizializzatori tramite vincoli. Parametri e argomenti generici
Questo assegna al componente due responsabilità chiare: il suo body gestisce l’aspetto condiviso del pannello e il suo inizializzatore determina il contenuto inserito al suo interno. Al contenuto predefinito viene assegnato un tipo di vista con nome, così che l’inizializzatore di convenienza possa riferirsi a una specializzazione stabile e leggibile.
Crea un esempio macOS completo
Per questa guida, usa un target app SwiftUI per macOS con una versione minima di macOS 11 o successiva e un compilatore Swift 5.7 o più recente. La versione minima di macOS è necessaria per la struttura App e WindowGroup usata di seguito; Apple documenta WindowGroup come contenitore della scena per la gerarchia di viste di una finestra. WindowGroup
Sostituisci il punto di ingresso dell’app generato con questo file sorgente completo e rimuovi il file ContentView.swift generato se dichiara un altro ContentView. Mantieni una sola dichiarazione @main nel target. Tutti i valori mostrati di seguito sono esempi locali sintetici.
import SwiftUI
struct DefaultPanelContent: View {
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Sample workspace")
.font(.headline)
Text("Choose a layout to begin.")
.foregroundColor(.secondary)
}
}
}
struct DesktopPanel<Content: View>: View {
private let title: String
private let content: Content
init(
title: String,
@ViewBuilder content: () -> Content
) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.title2)
Divider()
content
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.08))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
}
}
extension DesktopPanel where Content == DefaultPanelContent {
init(title: String) {
self.init(title: title) {
DefaultPanelContent()
}
}
}
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 20) {
DesktopPanel(title: "Default panel")
DesktopPanel(title: "Custom panel") {
HStack {
Text("Sample items")
Spacer()
Text("3")
.font(.headline)
}
Text("Layout: Compact")
.foregroundColor(.secondary)
}
}
.padding(24)
.frame(minWidth: 420, minHeight: 300)
}
}
@main
struct PanelExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Compila ed esegui il target macOS. Il risultato previsto è costituito da due pannelli con lo stesso stile esterno: il primo contiene il testo dell’area di lavoro predefinita, il secondo una riga con un conteggio seguita da una descrizione del layout. Questo ambiente non dispone di un compilatore Swift né dell’SDK Apple, quindi l’esempio è stato esaminato ma non è stato possibile compilarlo.
Le indicazioni di Apple sulle viste personalizzate supportano questa composizione: dichiarare un conformer a View, implementarne il body e assemblare viste integrate e personalizzate in una gerarchia. Spiegano inoltre che i modificatori restituiscono viste modificate, motivo per cui lo stile condiviso deve essere applicato attorno al contenuto del pannello. Dichiarare una vista personalizzata
Perché l’inizializzatore vincolato risolve il tipo generico
L’inizializzatore personalizzato riceve una closure che restituisce Content. La chiamata a content() costruisce il valore memorizzato nel pannello. La closure viene utilizzata durante l’inizializzazione, quindi questa implementazione non la memorizza e non richiede @escaping.
Per la chiamata senza builder, l’estensione rende disponibile init(title:) specificamente quando Content è DefaultPanelContent. Questo requisito fornisce le informazioni mancanti sul tipo. L’implementazione delega quindi all’inizializzatore basato su builder, mantenendo l’inizializzazione delle proprietà in un unico punto.
Il valore predefinito risultante è di tipo DesktopPanel<DefaultPanelContent>. Il tipo di contenuto del pannello personalizzato viene inferito dalla sua closure. Chi lo usa non deve specificare quel tipo potenzialmente complesso.
Una conseguenza utile è che un’istanza predefinita è già specializzata. Non puoi assegnare a una stessa variabile un pannello con un tipo di contenuto diverso solo perché entrambi i valori visualizzano pannelli. Se l’interfaccia richiede presentazioni alternative, crea un ramo basato su builder nella vista che li contiene.
L’attributo builder consente la closure personalizzata con più istruzioni nell’esempio. Permette a HStack e al successivo Text di partecipare alla costruzione del contenuto; non sostituisce il requisito Content: View del componente. Apple documenta la gestione di più espressioni e del contenuto condizionale da parte del builder. ViewBuilder
Quando un singolo inizializzatore builder con argomento predefinito può sostituire l’overload?
Swift 5.7 ha implementato SE-0347, che consente a un’espressione predefinita concreta per un argomento omesso di inferire un tipo generico. Per questo componente, offre un’alternativa più breve. SE-0347: inferenza del tipo dalle espressioni predefinite
Con un compilatore Swift 5.7 o successivo, sostituisci l’inizializzatore basato su builder all’interno di DesktopPanel con il frammento seguente e rimuovi l’intera estensione vincolata. Mantieni l’importazione, le proprietà memorizzate, DefaultPanelContent, body e il codice dell’app dell’esempio completo.
// Replacement initializer inside DesktopPanel.
init(
title: String,
@ViewBuilder content: () -> Content = {
DefaultPanelContent()
}
) {
self.title = title
self.content = content()
}
Ora, se ometti la closure, Content viene inferito da DefaultPanelContent(). Se invece fornisci una closure, il tipo viene inferito da quell’argomento. Si tratta di inferenza da espressione predefinita, non di un argomento di tipo predefinito nella dichiarazione del componente.
La proposta limita questa inferenza: il parametro generico interessato deve comparire in un’unica posizione nell’elenco dei parametri e i requisiti di uguaglianza di tipo correlati devono rispettare le relative regole. Qui Content compare solo nel parametro builder. Riutilizzarlo in un altro parametro dell’inizializzatore può rendere inapplicabile questo approccio. Progettazione dettagliata di SE-0347
Scegli l’overload quando il contenuto predefinito richiede argomenti dell’inizializzatore. Per esempio, una sottotitolo predefinito derivato da title può essere costruito all’interno di init(title:) e passato all’inizializzatore basato su builder. Un’espressione di argomento predefinito non può acquisire il title fornito come se fosse una closure creata nel corpo di quell’inizializzatore.
| Requisito del componente | Interfaccia adatta |
|---|---|
| Contenuto predefinito fisso e compilatore Swift 5.7+ | Un parametro builder con valore predefinito è conciso. |
| Contenuto predefinito costruito a partire da altri argomenti dell’inizializzatore | Usa un inizializzatore vincolato e costruisci il valore al suo interno. |
| Diverse presentazioni predefinite con nome | Usa overload espliciti dell’inizializzatore con etichette e vincoli chiari. |
| Un tipo memorizzato esplicito per la specializzazione predefinita | Nomina DesktopPanel<DefaultPanelContent> o introduci un alias di tipo. |
Non cercare di esprimere questa interfaccia come struct DesktopPanel<Content: View = DefaultPanelContent>. La grammatica attuale dei parametri generici di Swift non prevede questa sintassi per i tipi predefiniti. I percorsi supportati in questo caso sono l’inizializzazione vincolata e l’inferenza da espressione predefinita. Parametri e argomenti generici
Mantieni l’interfaccia compatibile tra le versioni del builder
La documentazione attuale di Apple distingue Xcode 26 e precedenti da Xcode 27 e successivi. Con la toolchain più recente, ViewBuilder costruisce contenuto indipendente dal tipo; Apple introduce ContentBuilder, un alias di tipo per ViewBuilder, come sintassi per questo comportamento unificato. Questa distinzione dipende dalla toolchain di compilazione e non semplicemente dalla versione minima di distribuzione dell’app. ViewBuilder, ContentBuilder
L’esempio mantiene @ViewBuilder per la compatibilità con le toolchain precedenti. Quando adotti la sintassi più recente in Xcode 27 o successivo, cambia l’attributo del parametro in @ContentBuilder. Mantieni Content: View: il pannello ha ancora bisogno di un contenuto che possa inserire in una gerarchia di viste.
Evita di specificare la specializzazione predefinita come tipo annidato di stack e tupla. La nota di migrazione di Apple documenta il passaggio da TupleView a TupleContent nei builder con più espressioni, che può rendere non valide le condizioni che nominano queste strutture generate. L’esempio vincola invece l’estensione a DefaultPanelContent; il layout interno resta racchiuso in body: some View. TN3211: risoluzione delle incompatibilità del codice sorgente SwiftUI per State e ContentBuilder
Questo wrapper con nome semplifica anche le modifiche ordinarie. Aggiungere una terza riga al contenuto predefinito ne modifica l’implementazione senza costringerti a riscrivere un lungo tipo concreto nell’inizializzatore di convenienza.
Diagnostica il contratto prima di modificare il layout
Se il componente non si compila, individua innanzitutto quale parte dell’interfaccia non dispone di informazioni sul tipo. Modificare la spaziatura interna o racchiudere tutto in un altro stack non risolverà una specializzazione generica mancante.
- La chiamata senza builder non riesce a inferire
Content. Verifica che esista ilinit(title:)vincolato oppure che l’inizializzatore con valore predefinito venga compilato con Swift 5.7 o successivo. - Un valore predefinito concreto non può essere assegnato a
Content. Verifica che l’assegnazione avvenga all’interno dell’estensione vincolata. La sola conformità aViewnon rendeDefaultPanelContentintercambiabile con ogni possibileContent. - Le espressioni personalizzate multiple vengono rifiutate. Verifica che il parametro closure abbia l’attributo builder.
- Un tipo esplicito rifiuta il contenuto personalizzato. Un’annotazione
DesktopPanel<DefaultPanelContent>fissa intenzionalmente il tipo di contenuto. Rimuovi l’annotazione quando la chiamata deve inferire una specializzazione diversa. - Un aggiornamento della toolchain rompe un vincolo basato su tuple. Sostituisci la dipendenza dalla struttura generata dal builder con una vista predefinita con nome oppure segui la migrazione documentata da Apple per il vincolo specifico.
Infine, mantieni leggero il builder. In questa implementazione viene eseguito ogni volta che il pannello viene inizializzato; non garantisce un’esecuzione una tantum. Apple osserva che SwiftUI può reinizializzare le viste e consiglia di evitare operazioni impegnative nei loro inizializzatori. Dichiarare una vista personalizzata
Usa l’inizializzatore vincolato come punto di partenza esplicito: rende visibile la specializzazione predefinita e consente di inferire facilmente il contenuto personalizzato. Adotta l’unico inizializzatore con valore predefinito quando i requisiti del compilatore e le restrizioni di inferenza sono compatibili con la tua API. Entrambi gli approcci permettono al pannello macOS di mantenere il proprio aspetto condiviso, mentre chi lo usa fornisce solo il contenuto necessario.
Fonti
- Parametri e argomenti generici — Swift.org
- Inferenza del tipo dalle espressioni predefinite — Swift Evolution
- Dichiarare una vista personalizzata — Documentazione per sviluppatori Apple
- ViewBuilder — Documentazione per sviluppatori Apple
- ContentBuilder — Documentazione per sviluppatori Apple
- WindowGroup — Documentazione per sviluppatori Apple
- TN3211: risoluzione delle incompatibilità del codice sorgente SwiftUI per State e ContentBuilder — Documentazione per sviluppatori Apple