Per un pannello SwiftUI riutilizzabile in un’app Mac, metti per primo il builder del contenuto principale, assegna etichette agli altri builder e usa inizializzatori con vincoli per le aree omesse. In questo modo le chiamate sono leggibili e le intestazioni e i piè di pagina assenti hanno tipi espliciti.
Questo tutorial crea un piccolo componente con aree per il contenuto, l’intestazione e il piè di pagina. L’obiettivo è supportare quattro chiamate utili — solo contenuto, contenuto con uno dei due elementi accessori e contenuto con entrambi — senza dipendere da closure predefinite in competizione tra loro. Tutti i valori visualizzati sono testi locali sintetici.
Cosa rende non ambiguo un inizializzatore con più builder?
La sintassi Swift per più closure finali omette l’etichetta dell’argomento della prima closure finale e richiede etichette per quelle successive. Di conseguenza, un componente dovrebbe rendere evidente lo scopo del primo builder dal nome del componente e assegnare etichette distinte ai builder successivi. Questa sintassi è stata implementata in Swift 5.3. SE-0279: Multiple Trailing Closures
Per questo pannello, l’inizializzatore completo è init(content:header:footer:). Nel punto in cui viene chiamato, il primo blocco fornisce il contenuto; header: e footer: identificano gli altri blocchi. L’ordine nella dichiarazione determina l’ordine degli argomenti, mentre body del componente determina l’ordine visivo. Il contenuto può quindi essere il primo argomento anche se l’intestazione appare sopra di esso.
L’interfaccia attuale di Section di Apple segue questo schema, con il contenuto per primo. Il suo inizializzatore completo accetta tre closure builder e inizializzatori separati supportano le combinazioni con aree assenti. È un utile precedente per l’interfaccia; il pannello personalizzato qui sotto ha un layout e un comportamento propri. Apple: Section, Apple: init(content:header:footer:)
Un attributo builder e un’etichetta di parametro risolvono problemi diversi. Un attributo result builder trasforma il corpo della closure fornita; le etichette degli argomenti determinano come chi chiama identifica i parametri. Aggiungere @ViewBuilder non ripristina la prima etichetta quando la sintassi delle closure finali la omette. SE-0289: Result builders
Questa distinzione suggerisce una regola pratica di progettazione: evita inizializzatori affini la cui unica distinzione utile scompare quando viene omessa l’etichetta della prima closure finale. Per esempio, due inizializzatori altrimenti equivalenti chiamati init(content:header:) e init(body:header:) presenterebbero lo stesso primo blocco senza etichetta, seguito da header:. È una conseguenza delle regole sulle closure finali, non un motivo per aggiungere altri attributi builder.
Quali limiti di compilatore e SwiftUI è importante considerare?
Tieni presenti tre aspetti: il compilatore, la modalità del linguaggio Swift selezionata e l’SDK SwiftUI. Le closure finali multiple sono state introdotte in Swift 5.3, mentre la funzionalità formale dei result builder è stata implementata in Swift 5.4. Queste tappe descrivono il supporto del linguaggio; non stabiliscono la disponibilità di ogni inizializzatore del framework. SE-0279, SE-0289
La documentazione attuale di Apple definisce anche un limite legato alla toolchain: Xcode 26 e versioni precedenti usano il comportamento consolidato per la creazione delle viste, mentre Xcode 27 e versioni successive creano contenuti indipendenti dal tipo a partire da questi builder. Per il comportamento più recente, Apple indica di usare la forma @ContentBuilder. ContentBuilder è un alias di tipo per ViewBuilder; i vincoli dell’API che lo contiene determinano comunque quali contenuti sono accettabili. Apple: ViewBuilder, Apple: ContentBuilder
L’esempio completo qui sotto usa la forma @ViewBuilder come riferimento per Xcode 26 e richiede macOS 11 o versioni successive. Se adotti lo stile d’interfaccia documentato per Xcode 27, sostituisci le annotazioni dei parametri dell’inizializzatore con @ContentBuilder. Mantieni i tre vincoli generici View: questo componente inserisce tutte e tre le aree in un layout di viste.
Evita di indicare nel contratto del componente il tipo della tupla generata da un builder. Apple documenta incompatibilità del codice sorgente che coinvolgono vincoli TupleView espliciti quando si adotta la nuova implementazione dei builder. I tipi generici delle aree permettono a questo pannello di evitare tale dipendenza. Apple: TN3211, Resolving SwiftUI source incompatibilities for State and ContentBuilder
Crea un pannello con quattro forme esplicite di inizializzatore
Usa un tipo generico indipendente per ogni area. Riutilizzare un solo parametro generico richiederebbe che le aree avessero lo stesso tipo concreto, una restrizione non necessaria quando una contiene una singola vista di testo e un’altra ne contiene diverse.
Per un’area assente, specializza il suo parametro generico a EmptyView. Apple documenta proprio questo ruolo per EmptyView: rappresentare un elemento figlio assente in un tipo di vista generico. Gli inizializzatori con vincoli qui sotto rendono esplicita questa scelta invece di chiedere a chi chiama di inferire il tipo non specificato di un elemento accessorio. Apple: EmptyView
I due blocchi Swift di questa sezione formano un unico file applicativo completo. In un target di app SwiftUI per macOS, sostituisci le dichiarazioni generate dell’app e della vista dei contenuti con questi blocchi, mantenendo una sola dichiarazione @main. Il primo blocco definisce il componente e tutte e quattro le forme dell’inizializzatore.
import SwiftUI
struct BuilderPanel<Content: View, Header: View, Footer: View>: View {
private let content: Content
private let header: Header
private let footer: Footer
init(
@ViewBuilder content: () -> Content,
@ViewBuilder header: () -> Header,
@ViewBuilder footer: () -> Footer
) {
self.content = content()
self.header = header()
self.footer = footer()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
header
content
footer
}
.padding()
}
}
extension BuilderPanel where Footer == EmptyView {
init(
@ViewBuilder content: () -> Content,
@ViewBuilder header: () -> Header
) {
self.init(
content: content,
header: header,
footer: { EmptyView() }
)
}
}
extension BuilderPanel where Header == EmptyView {
init(
@ViewBuilder content: () -> Content,
@ViewBuilder footer: () -> Footer
) {
self.init(
content: content,
header: { EmptyView() },
footer: footer
)
}
}
extension BuilderPanel where Header == EmptyView, Footer == EmptyView {
init(@ViewBuilder content: () -> Content) {
self.init(
content: content,
header: { EmptyView() },
footer: { EmptyView() }
)
}
}
L’inizializzatore completo chiama ogni builder una volta durante l’inizializzazione di quel valore del pannello e memorizza i valori restituiti. Gli inizializzatori di comodità delegano a esso. Questa tempistica dipende da questa implementazione: non garantisce che un builder venga eseguito una sola volta nell’intero ciclo di vita dell’interfaccia, perché la creazione di un altro valore del pannello richiama il suo inizializzatore.
Aggiungi questo secondo blocco allo stesso file. Esegue tutte le forme supportate, inclusi più statement in un singolo builder.
struct PanelDemo: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
BuilderPanel {
Text("Canvas A")
}
BuilderPanel {
Text("Canvas B")
Text("Grid: 12 columns")
} header: {
Text("Layout")
}
BuilderPanel {
Text("Canvas C")
} footer: {
Text("Units: points")
}
BuilderPanel {
Text("Canvas D")
} header: {
Text("Preview")
} footer: {
Text("Scale: 100 percent")
}
}
.padding()
}
.frame(minWidth: 360, minHeight: 440)
}
}
@main
struct BuilderPanelDemoApp: App {
var body: some Scene {
WindowGroup {
PanelDemo()
}
}
}
Compila ed esegui l’app in Xcode per convalidare il contratto rispetto all’SDK selezionato. Il codice è stato esaminato, ma qui non è stato compilato né eseguito; l’ambiente di ricerca non disponeva né di un compilatore Swift né dell’SDK SwiftUI per macOS.
Associa ogni chiamata a un contratto di tipo concreto
Una tabella delle forme di chiamata è un utile strumento decisionale. Indica quale inizializzatore dovrebbe essere selezionato e quale tipo concreto viene assegnato alle aree mancanti. Sono aspettative ricavate dalle dichiarazioni dell’esempio, pronte per essere verificate nel tuo progetto.
| Aree fornite | Etichette dell’inizializzatore | Vincoli per le aree assenti |
|---|---|---|
| Solo contenuto | content: |
Header == EmptyView, Footer == EmptyView |
| Contenuto e intestazione | content:header: |
Footer == EmptyView |
| Contenuto e piè di pagina | content:footer: |
Header == EmptyView |
| Tutte e tre | content:header:footer: |
Nessuno |
Ogni inizializzatore più breve richiede tutte le closure che dichiara. Nessun argomento predefinito fa competere l’inizializzatore con tre builder per la chiamata con il solo contenuto. Le forme con solo intestazione e solo piè di pagina si distinguono per la seconda etichetta, anche quando entrambe le closure restituiscono Text.
Usa questa tabella quando modifichi il componente: se un nuovo inizializzatore accetta una forma di chiamata esistente, verifica se la nuova distinzione resta visibile dopo l’omissione della prima etichetta della closure finale. In questo modo puoi individuare le sovrapposizioni dell’interfaccia prima che generino un messaggio diagnostico difficile da interpretare in una vista più grande.
Perché evitare più closure builder con valori predefiniti?
Gli argomenti predefiniti sono utili, ma diversi parametri closure con valori predefiniti introducono regole di corrispondenza che dipendono dalla modalità del linguaggio Swift. SE-0286 documenta la corrispondenza in avanti, un’euristica di compatibilità per saltare i parametri predefiniti e il ripiego alla corrispondenza all’indietro nelle modalità precedenti. Documenta anche il flag di funzionalità futura ForwardTrailingClosures, implementato in Swift 5.8. SE-0286: Forward-scan matching for trailing closures
La raccomandazione pratica qui è volutamente più circoscritta di quelle regole: un piccolo pannello riutilizzabile può evitare di dipendere da esse. Rendi obbligatorio il suo builder principale ed esprimi le omissioni supportate tramite inizializzatori con vincoli. Così le combinazioni supportate risultano visibili nelle dichiarazioni e il significato previsto del componente non dipende dal comportamento di corrispondenza, che può variare in base alla versione del compilatore.
Anche un elemento accessorio omesso e una closure accessoria vuota rappresentano scelte d’interfaccia diverse. L’inizializzatore con il solo contenuto seleziona esplicitamente EmptyView per entrambi gli elementi accessori. Fornire un builder vuoto lascia all’implementazione del builder dell’SDK il compito di determinare il risultato della closure. Per questo componente, ometti le aree inutilizzate e lascia che l’inizializzatore di comodità fornisca il tipo documentato per l’assenza.
Questo approccio ha un limite di scala. Tre aree producono quattro combinazioni utili quando il contenuto è sempre obbligatorio. Un componente con molte aree indipendenti opzionali richiederebbe una famiglia di overload molto più ampia. Prima di aggiungerli, decidi quali combinazioni servono davvero a chi legge; un insieme più ridotto di varianti significative del componente può essere più facile da usare e mantenere.
Come diagnosticare una chiamata ambigua?
Inizia dalla chiamata minima del pannello che riproduce il problema. Poi segui questa sequenza, che distingue la corrispondenza degli argomenti dagli errori all’interno di un builder.
- Scrivi tutti gli argomenti closure tra parentesi. Specifica
content:,header:efooter:nell’ordine delle dichiarazioni. Se la chiamata si risolve, controlla quale distinzione è stata rimossa dalla sintassi finale. - Sostituisci temporaneamente ogni area con una sola espressione
Text. Se la chiamata funziona, ripristina una alla volta le espressioni originali per individuare il corpo del builder che causa l’errore. - Confronta le forme degli inizializzatori. Cerca valori predefiniti in competizione, ordini delle closure invertiti e overload distinti soltanto dall’etichetta del primo builder.
- Controlla i vincoli delle aree assenti. Ogni area generica omessa dovrebbe avere un tipo concreto previsto nell’inizializzatore selezionato.
- Controlla i limiti della toolchain. Dopo il passaggio a Xcode 27, verifica i vincoli espliciti sui risultati dei builder e gli eventuali nomi di tipo in conflitto prima di riprogettare la famiglia di inizializzatori.
Scrivere le etichette tra parentesi conserva i nomi degli argomenti omessi dalla prima closure finale. Swift supporta entrambe le forme, quindi la forma interamente etichettata è utile per diagnosticare i problemi e può essere una scelta appropriata anche nel codice definitivo, quando rende una particolare chiamata più leggibile. The Swift Programming Language: Closures
Per i messaggi diagnostici degli SDK più recenti, Apple consiglia nello specifico di qualificare i tipi in conflitto tra moduli e aggiornare i vincoli che dipendono dai vecchi tipi di output delle tuple. Questi problemi possono verificarsi all’interno di una chiamata a un inizializzatore ben progettata; aggiungere altri overload non risolverebbe il problema sottostante. Apple: TN3211
Un contratto chiaro per l’inizializzatore parte dalle chiamate che il componente deve supportare. Metti per primo il contenuto principale, assegna etichette significative ai builder accessori e assegna tipi espliciti alle aree assenti. Poi compila ogni forma di chiamata supportata con il compilatore, la modalità del linguaggio e l’SDK selezionati prima di ampliare l’interfaccia.
Fonti
- Multiple Trailing Closures — SE-0279 — Swift Project.
- Forward-scan matching for trailing closures — SE-0286 — Swift Project.
- Result builders — SE-0289 — Swift Project.
- Closures — The Swift Programming Language — Swift Project.
- Section — Apple.
- init(content:header:footer:) — Apple.
- ViewBuilder — Apple.
- ContentBuilder — Apple.
- EmptyView — Apple.
- TN3211: Resolving SwiftUI source incompatibilities for State and ContentBuilder — Apple.