Para um painel SwiftUI reutilizável em um app para Mac, coloque primeiro o builder do conteúdo principal, rotule os demais builders e use inicializadores com restrições para as regiões omitidas. Isso mantém as chamadas legíveis e atribui tipos explícitos a cabeçalhos e rodapés ausentes.
Este tutorial cria um pequeno componente com regiões de conteúdo, cabeçalho e rodapé. O objetivo é oferecer quatro chamadas úteis — somente conteúdo, conteúdo com cada um dos elementos auxiliares e conteúdo com ambos — sem depender de closures com valores padrão concorrentes. Todos os valores exibidos são textos locais sintéticos.
O que torna inequívoco um inicializador com múltiplos builders?
A sintaxe de múltiplas closures finais do Swift omite o rótulo do argumento da primeira closure final e exige rótulos nas closures finais seguintes. Por isso, um componente deve deixar evidente a finalidade do primeiro builder pelo nome do componente e atribuir rótulos distintos aos builders posteriores. Essa sintaxe foi implementada no Swift 5.3. SE-0279: Múltiplas closures finais
Para este painel, o inicializador completo é init(content:header:footer:). No ponto de chamada, o primeiro bloco fornece o conteúdo; header: e footer: identificam os outros blocos. A ordem na declaração controla a ordem dos argumentos, enquanto body do componente controla a ordem visual. Assim, o conteúdo pode ser o primeiro argumento mesmo que o cabeçalho apareça acima dele.
A interface atual de Section da Apple segue essa organização com o conteúdo primeiro. Seu inicializador completo aceita três closures builder, e inicializadores separados dão suporte a combinações com regiões ausentes. Este é um precedente útil de interface; o painel personalizado abaixo tem seu próprio layout e comportamento. Apple: Section, Apple: init(content:header:footer:)
Um atributo builder e um rótulo de parâmetro resolvem problemas diferentes. Um atributo de result builder transforma o corpo da closure fornecida; os rótulos dos argumentos determinam como os chamadores identificam os parâmetros. Adicionar @ViewBuilder não restaura o primeiro rótulo quando a sintaxe de closure final o omite. SE-0289: Result builders
Essa distinção leva a uma regra prática de projeto: evite inicializadores relacionados cuja única distinção útil desaparece quando o rótulo da primeira closure final é omitido. Por exemplo, dois inicializadores equivalentes chamados init(content:header:) e init(body:header:) apresentariam o mesmo primeiro bloco sem rótulo, seguido por header:. Isso é uma consequência de projeto das regras de closures finais, não um motivo para adicionar mais anotações builder.
Quais limites do compilador e do SwiftUI são relevantes?
Tenha em mente três detalhes: o compilador, o modo de linguagem Swift selecionado e o SDK do SwiftUI. As múltiplas closures finais chegaram no Swift 5.3, enquanto o recurso formal de result builder foi implementado no Swift 5.4. Esses marcos descrevem o suporte da linguagem; não estabelecem a disponibilidade de cada inicializador de framework. SE-0279, SE-0289
A documentação atual da Apple também estabelece um limite entre toolchains: o Xcode 26 e versões anteriores usam o comportamento consolidado de composição de views, enquanto o Xcode 27 e versões posteriores constroem conteúdo independente de tipo a partir desses builders. Para o novo comportamento, a Apple direciona os desenvolvedores à grafia @ContentBuilder. ContentBuilder é um alias de tipo para ViewBuilder; as restrições da API envolvente ainda determinam qual conteúdo é aceitável. Apple: ViewBuilder, Apple: ContentBuilder
O exemplo completo abaixo usa a grafia @ViewBuilder como referência para o Xcode 26 e tem como alvo o macOS 11 ou posterior. Ao adotar o estilo de interface documentado para o Xcode 27, substitua as anotações dos parâmetros do inicializador por @ContentBuilder. Mantenha as três restrições genéricas View: este componente posiciona as três regiões dentro de um layout de view.
Evite nomear no contrato do componente o tipo de tupla gerado por um builder. A Apple documenta incompatibilidades de código-fonte envolvendo restrições explícitas de TupleView ao adotar a nova implementação de builder. Tipos genéricos para as regiões permitem que este painel evite essa dependência. Apple: TN3211, Resolução de incompatibilidades de código-fonte do SwiftUI para State e ContentBuilder
Crie um painel com quatro formas explícitas de inicializador
Use um tipo genérico independente para cada região. Reutilizar um único parâmetro genérico exigiria que as regiões tivessem o mesmo tipo concreto, uma restrição desnecessária quando uma contém uma única view de texto e outra contém várias views.
Para uma região ausente, especialize seu parâmetro genérico como EmptyView. A Apple documenta exatamente essa função para EmptyView: representar um filho ausente em um tipo de view genérico. Os inicializadores com restrições abaixo tornam essa escolha explícita, em vez de pedir aos chamadores que deduzam um tipo auxiliar não especificado. Apple: EmptyView
Os dois blocos Swift desta seção formam um único arquivo completo de aplicação. Em um destino de app SwiftUI para macOS, substitua as declarações geradas do app e da view de conteúdo por estes blocos, mantendo exatamente uma declaração @main. O primeiro bloco define o componente e as quatro formas de inicializador.
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() }
)
}
}
O inicializador completo chama cada builder uma vez durante a inicialização daquele valor do painel e armazena os valores retornados. Os inicializadores de conveniência delegam a ele. Esse momento é uma característica desta implementação: não garante que um builder seja executado apenas uma vez durante a vida da interface, pois criar outro valor do painel chama seu inicializador novamente.
Acrescente este segundo bloco ao mesmo arquivo. Ele exercita todas as formas aceitas, incluindo várias instruções em um único 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()
}
}
}
Compile e execute o app no Xcode para validar o contrato com o SDK selecionado. O código foi inspecionado, mas não foi compilado nem executado aqui; o ambiente de pesquisa não tinha compilador Swift nem o SDK SwiftUI do macOS.
Associe cada chamada a um contrato de tipo concreto
Uma tabela de formatos de chamada é uma ferramenta útil para decidir. Ela indica qual inicializador deve ser selecionado e quais tipos concretos são atribuídos às regiões ausentes. Estas são expectativas derivadas das declarações do exemplo, prontas para verificação no seu projeto.
| Regiões fornecidas | Rótulos do inicializador | Restrições das regiões ausentes |
|---|---|---|
| Somente conteúdo | content: |
Header == EmptyView, Footer == EmptyView |
| Conteúdo e cabeçalho | content:header: |
Footer == EmptyView |
| Conteúdo e rodapé | content:footer: |
Header == EmptyView |
| As três regiões | content:header:footer: |
Nenhuma |
Cada inicializador mais curto exige todas as closures que declara. Nenhum argumento padrão faz o inicializador de três builders concorrer com a chamada somente de conteúdo. As formas com apenas cabeçalho ou apenas rodapé são diferenciadas pelos rótulos do segundo argumento, mesmo quando ambas as closures retornam Text.
Use esta tabela ao alterar o componente: se um novo inicializador aceitar um formato de chamada existente, verifique se a nova distinção continua visível depois que o primeiro rótulo de closure final desaparece. Isso detecta sobreposições na interface antes que se tornem diagnósticos difíceis em uma view maior.
Por que evitar vários builders com valores padrão?
Argumentos padrão são úteis, mas vários parâmetros closure com valores padrão introduzem regras de correspondência que dependem do modo de linguagem Swift. A SE-0286 documenta a correspondência para a frente, uma heurística de compatibilidade que permite pular parâmetros com valores padrão, e o fallback de correspondência para trás em modos mais antigos. Ela também documenta a flag de recurso futuro ForwardTrailingClosures, implementada no Swift 5.8. SE-0286: Correspondência com varredura para a frente de closures finais
A recomendação prática aqui é deliberadamente mais restrita do que essas regras: um pequeno painel reutilizável pode evitar depender delas. Mantenha obrigatório o builder principal e expresse as omissões aceitas por meio de inicializadores com restrições. Isso torna visíveis nas declarações as combinações aceitas e evita usar um comportamento de correspondência sensível à versão do compilador como parte do significado pretendido do componente.
Um elemento auxiliar omitido e uma closure de elemento auxiliar vazia também são escolhas de interface diferentes. O inicializador somente de conteúdo seleciona explicitamente EmptyView para ambos os elementos auxiliares. Fornecer um builder vazio deixa a implementação de builder do SDK determinar o resultado da closure. Para este componente, omita as regiões não usadas e deixe o inicializador de conveniência fornecer o tipo de ausência documentado.
Esta abordagem tem um limite de escala. Três regiões produzem quatro combinações úteis quando o conteúdo é sempre obrigatório. Um componente com muitas regiões opcionais independentes exigiria uma família muito maior de sobrecargas. Antes de adicionar essas sobrecargas, decida quais combinações os leitores realmente precisam; um conjunto menor de variantes significativas do componente pode ser mais fácil de usar e manter.
Como diagnosticar uma chamada ambígua?
Comece com a menor chamada do painel que reproduz o problema. Em seguida, siga esta sequência; ela separa a correspondência de argumentos das falhas dentro de um builder.
- Escreva todos os argumentos closure entre parênteses. Especifique
content:,header:efooter:na ordem da declaração. Se isso resolver a chamada, examine qual distinção foi removida pela sintaxe de closure final. - Substitua temporariamente cada região por uma única expressão
Text. Se a chamada funcionar, restaure as expressões originais uma de cada vez para localizar o corpo do builder que falha. - Compare as formas dos inicializadores. Procure valores padrão concorrentes, ordens de closure invertidas e sobrecargas diferenciadas apenas pelo rótulo do primeiro builder.
- Verifique as restrições das regiões ausentes. Cada região genérica omitida deve ter um tipo concreto pretendido no inicializador selecionado.
- Verifique o limite do toolchain. Após migrar para o Xcode 27, inspecione as restrições explícitas sobre os resultados dos builders e os nomes de tipos conflitantes antes de redesenhar a família de inicializadores.
Escrever os rótulos entre parênteses preserva os nomes dos argumentos que a primeira closure final omite. O Swift aceita ambas as formas, então a forma totalmente rotulada é útil para diagnóstico e também pode ser a grafia final adequada quando torna uma chamada específica mais legível. A Linguagem de Programação Swift: Closures
Para diagnósticos em SDKs mais recentes, a Apple recomenda especificamente qualificar tipos que entram em conflito entre módulos e atualizar restrições que dependem de tipos de saída de tupla mais antigos. Esses problemas podem ocorrer dentro de uma chamada de inicializador bem projetada; adicionar mais sobrecargas não resolveria a questão subjacente. Apple: TN3211
Um contrato de inicializador claro começa com as chamadas que o componente precisa aceitar. Coloque primeiro o conteúdo principal, dê rótulos significativos aos builders auxiliares e atribua tipos explícitos às regiões ausentes. Em seguida, compile cada formato de chamada aceito com o compilador, o modo de linguagem e o SDK selecionados antes de expandir a interface.
Fontes
- Múltiplas closures finais — SE-0279 — Projeto Swift.
- Correspondência com varredura para a frente de closures finais — SE-0286 — Projeto Swift.
- Result builders — SE-0289 — Projeto Swift.
- Closures — A Linguagem de Programação Swift — Projeto Swift.
- Section — Apple.
- init(content:header:footer:) — Apple.
- ViewBuilder — Apple.
- ContentBuilder — Apple.
- EmptyView — Apple.
- TN3211: Resolução de incompatibilidades de código-fonte do SwiftUI para State e ContentBuilder — Apple.