Do que o SwiftUI é feito
Gênero: framework-explainer. O post explica o substrato sobre o qual o SwiftUI se apoia: result builders, tipos de retorno opacos e uma árvore de views por valor. Quando o substrato fica visível, as partes do SwiftUI que surpreendem desenvolvedores (AnyView, Group, ViewBuilder, parâmetros @ViewBuilder, o temido erro some View vs any View) deixam de ser misteriosas.
Uma view do SwiftUI é um tipo por valor que está em conformidade com um único protocolo com um único requisito. O resto do framework é construído sobre recursos da linguagem Swift que existem fora do SwiftUI: result builders, tipos opacos, generics com restrições, property wrappers. Se você entende os recursos da linguagem, o framework se lê como código Swift API normal. Se não, o framework se lê como mágica que ocasionalmente morde.
O post percorre o substrato. Não há LiveActivityManager aqui, nenhum screenshot do Get Bananas. O ponto é o framework, não um projeto; quando o framework fica legível, todo post de shipped-code do cluster se lê de forma mais limpa.
TL;DR
- Uma view do SwiftUI é um tipo por valor do Swift em conformidade com
View. O protocolo tem um requisito:var body: some View { get }. Todo o resto é construído sobre recursos da linguagem Swift. @ViewBuilderé um result builder. O body de todaViewé um. Result builders transformam expressões separadas por vírgula em um único valor de retorno por meio de chamadas sintetizadas pelo compilador.some Viewé um tipo de retorno opaco. O compilador conhece o tipo concreto; o chamador não. O tipo opaco é o que faz com que os bodies das views sejam rápidos em tempo de compilação e de execução;AnyViewé a saída de emergência por type erasure para casos em que a opacidade não funciona.Group,EmptyView,TupleView,_ConditionalContentsão os tipos de implementação que os result builders sintetizam. Eles são documentados, mas raramente escritos à mão.
O protocolo que começa tudo
O protocolo View tem um requisito:1
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
Duas partes desse protocolo importam para entender o resto do SwiftUI.
O tipo associado Body : View. O body de uma view é, ele próprio, uma view. A recursão é o que torna o framework componível. Toda View retorna outra View a partir do seu body, e assim por diante, até a cadeia terminar em uma das primitive views do framework (como Text, Color, Image, EmptyView) cujo Body é Never. Primitive views são as folhas da árvore; as views que você escreve são os ramos.
O atributo @ViewBuilder em body. Todo body é uma closure de result builder. Result builders são um recurso da linguagem Swift documentado na SE-0289 (formalizado como @resultBuilder no Swift 5.4) que permite que uma closure com uma sequência de expressões seja transformada pelo compilador em um único valor de retorno por meio de chamadas de método sintetizadas.2 A transformação é o que faz funcionar a sintaxe sem vírgulas, em formato de declaração, dentro de um body do SwiftUI.
O formato do protocolo é incomum por dois motivos.
Primeiro, o requisito é uma propriedade computada, não um método. O body da view é recomputado a cada passada de renderização quando o SwiftUI decide que o estado da view mudou. O framework trata body como barato de chamar; computações longas dentro de body são um anti-padrão porque rodam a cada renderização.
Segundo, Self.Body é associado, não erased. O tipo concreto do body de uma view é parte de sua assinatura em tempo de compilação. O tipo do body de Text("Hello") é Never; o tipo do body de uma view customizada é o que @ViewBuilder sintetizou para o body. O design por tipo associado é o que permite ao compilador otimizar a árvore de views sem checagens de tipo em runtime. É também o que cria a exigência de some View quando uma view customizada retorna conteúdo condicional.
Result builders: a DSL sem vírgulas
Um result builder é um recurso da linguagem Swift que transforma uma closure em um único valor de retorno inserindo chamadas de método sintetizadas pelo compilador. @ViewBuilder é um result builder. O body de toda view do SwiftUI é a sua closure.2
Considere esta view:
struct ExampleView: View {
var body: some View {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
}
O body tem três declarações sem separador. Em Swift normal, isso é um erro de compilação: uma closure só pode retornar um valor. Result builders reescrevem a closure antes da compilação. O código real que o compilador vê, depois da expansão de @ViewBuilder, é aproximadamente:
struct ExampleView: View {
var body: some View {
ViewBuilder.buildBlock(
Text("Title"),
Text("Subtitle"),
Image(systemName: "star")
)
}
}
ViewBuilder.buildBlock(_:_:_:) é um método estático que recebe três views e retorna um TupleView<(Text, Text, Image)>. O body retorna esse único valor de tuple-view. Versões mais antigas do SwiftUI vinham com um conjunto fixo de overloads de buildBlock para 1, 2, 3, … até 10 filhos; o SwiftUI atual usa o suporte a variadic generics do Swift (buildBlock<each Content>), então um body com onze ou mais views irmãs não é mais um caso especial.
O mesmo padrão lida com fluxo de controle. Um body de view com uma instrução if se parece com isto:
struct ConditionalView: View {
let isActive: Bool
var body: some View {
if isActive {
Text("Active")
} else {
Text("Inactive")
}
}
}
@ViewBuilder reescreve isso por meio de chamadas a buildEither(first:) / buildEither(second:), produzindo um _ConditionalContent<Text, Text>. O compilador conhece o tipo de resultado em tempo de compilação, mesmo que apenas um ramo execute em qualquer renderização.
if let, switch, desempacotamento de opcionais e algumas outras construções são tratadas pelos vários métodos estáticos buildXxx do result builder.3 Conteúdo repetido é o único caso notável que o recurso da linguagem suporta via buildArray, mas que @ViewBuilder não suporta: um for cru dentro de um body falha com “closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.” A resposta no formato do SwiftUI é ForEach, que recebe uma RandomAccessCollection e uma closure de conteúdo e sintetiza a iteração como uma única view por valor. A DSL não é sob medida; são result builders do Swift, configurados para views.
some View e o problema da opacidade
O body de uma view customizada normalmente retorna some View. A palavra-chave é tipo de retorno opaco e foi adicionada no Swift 5.1.4
some View diz: “Eu retorno um tipo específico em conformidade com View, mas não estou dizendo qual.” O compilador rastreia o tipo concreto internamente para otimização; o chamador da sua view vê apenas a testemunha de protocolo. O padrão é o que permite que o body de uma view retorne um tipo complexo como VStack<TupleView<(Text, Image, Spacer)>> sem exigir que você escreva esse tipo no seu código-fonte.
Duas coisas sobre some View que confundem novos desenvolvedores SwiftUI:
some View é um tipo específico, mesmo quando você retorna coisas diferentes. A expressão if condition { Text("A") } else { Image("b") } é permitida dentro de um body @ViewBuilder porque o result builder envolve ambos os ramos em _ConditionalContent, produzindo um único tipo concreto. Mas a expressão if condition { return Text("A") } else { return Image("b") } fora de um result builder é um erro de compilação: os dois ramos retornam tipos concretos diferentes, e some View exige um. Result builders são o que faz funcionar formatos de retorno condicionais; returns explícitos perdem a transformação do result builder.
some View não é o mesmo que any View. some View é opaco (um tipo específico, escondido); any View é existencial (uma caixa que pode conter qualquer tipo em conformidade, com overhead de runtime). Uma função livre ou propriedade pode legalmente retornar any View. O body do protocolo View, no entanto, não pode: o protocolo exige associatedtype Body: View, e any View em si não está em conformidade com View, então var body: any View falha em satisfazer o protocolo e o compilador sugere some View. A regra prática: use some View para bodies de view, recorra a AnyView (o wrapper com type erasure) para tipos de view que variam em runtime. A mensagem de erro “function declares an opaque return type but the return statements in its body do not have matching underlying types” quase sempre significa que você tentou retornar tipos concretos diferentes de uma função some View e precisa ou de branching com result builder ou de AnyView.
AnyView: a saída de emergência
AnyView é um wrapper de view com type erasure. A construção é AnyView(myView). O wrapper segura qualquer view em conformidade, e o SwiftUI o aceita onde uma View é esperada.5
O caso de uso de saída de emergência é uma função que retorna tipos concretos diferentes com base em dados de runtime e que não pode ser expressa por meio de branching com result builder:
func viewForKind(_ kind: Kind) -> AnyView {
switch kind {
case .text: return AnyView(Text("hello"))
case .image: return AnyView(Image("photo"))
case .custom: return AnyView(MyCustomView())
}
}
O custo de AnyView é que o tipo subjacente não faz parte da identidade estática da view. A documentação da Apple descreve a consequência diretamente: quando o tipo embrulhado dentro de um AnyView muda entre renderizações, a hierarquia de view existente é destruída e uma nova hierarquia é criada em seu lugar, o que significa estado perdido, animações reiniciadas e identidade perdida. Reembrulhar o mesmo tipo concreto não dispara essa destruição, mas o diffing baseado em tipo estático que o framework prefere também não fica disponível de qualquer forma.
A regra certa é: prefira branching com result builder @ViewBuilder para views condicionais (if, switch, for), prefira views parametrizadas para tipos variáveis, recorra a AnyView apenas quando nenhuma das duas opções funciona. Uma func viewForKind que retorna AnyView geralmente é um sinal de que você deveria fazer viewForKind retornar some View e colocar um switch dentro de uma closure de result builder.
Group, EmptyView, TupleView: os tipos de implementação
O result builder sintetiza tipos concretos de view específicos. Três deles vale a pena reconhecer:6
Group é um container transparente. Aceita até dez views como conteúdo e as apresenta como irmãs ao layout pai. O container em si não adiciona estrutura visual; o conteúdo renderiza exatamente como renderizaria individualmente. O caso de uso é embrulhar múltiplas views em um contexto que espera uma única view (um modificador .if, um retorno condicional, uma função que produz “uma view”). Group { Text("A"); Text("B") } é uma única view contendo duas; é a forma explícita do que os result builders fazem implicitamente.
EmptyView é uma view que não renderiza nada. O result builder a usa como ramo falso condicional quando um if não tem else. Retornar EmptyView() do seu próprio código é uma forma de optar por não renderizar sem mudar o tipo de retorno da função.
TupleView é o tipo concreto que os result builders produzem quando um body tem múltiplas views irmãs. A expressão no topo deste post que retorna três views irmãs na verdade retorna um TupleView<(Text, Text, Image)>. Você quase nunca escreve TupleView diretamente; você o lê em mensagens de erro.
_ConditionalContent (com o sublinhado inicial) é o tipo que lida com ramos if/else. O tipo aparece na superfície pública do ViewBuilder, mas o nome com sublinhado sinaliza “não escreva contra isto casualmente”; deixe o result builder sintetizá-lo a partir de if/else em vez de construí-lo à mão.
@ViewBuilder em suas próprias funções
Result builders não são apenas para body. Qualquer função ou parâmetro de closure pode ser anotado com @ViewBuilder, e a mesma sintaxe DSL se torna válida dentro dele.2
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
content
}
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
// Usage: callers get the result-builder DSL inside the closure.
Card {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
Esse é o padrão pelo qual VStack, HStack, ZStack, List, Form, Section, Group e NavigationStack do próprio SwiftUI aceitam múltiplos filhos. Cada um desses tipos recebe um parâmetro @ViewBuilder content: () -> Content. Reconhecer o padrão significa que você pode escrever suas próprias container views com a mesma ergonomia das do framework, sem suporte especial do compilador.
A razão pela qual você escreve init(@ViewBuilder content:) e não apenas init(content:) é que o atributo no parâmetro é o que ativa a transformação do result builder dentro do body da closure que o chamador passa. Sem o atributo, Card { Text("A"); Text("B") } é um erro de compilação porque a closure tem duas declarações e nenhum @ViewBuilder para transformá-las.
Estado, bindings e a camada de property wrappers
Tudo acima é sobre o formato da árvore de views. A outra metade do SwiftUI é o estado, e essa metade é construída sobre property wrappers do Swift.7
Os property wrappers mais relevantes para autoria de views:
@State detém uma peça de estado por valor dentro de uma única view. Ler a propriedade lê o armazenamento subjacente; atribuir a ela dispara uma re-renderização da view. O wrapper é apropriado para estado simples e local da view (o on/off de um toggle, a string de rascunho de um campo de texto).
@Binding é uma referência bidirecional ao estado de outra view. Uma view filha que precisa ler e escrever o estado de um pai recebe um parâmetro Binding<T>. O pai constrói o binding por meio de $state (a projeção com cifrão em @State).
@Observable (iOS 17+) é uma macro que substitui o padrão mais antigo de conformidade com ObservableObject. A macro aplicada a uma classe gera rastreamento do framework Observation para que propriedades da classe disparem re-renderizações de view quando lidas dentro de um body e depois alteradas. A posse no lado da view de uma classe @Observable migra de @StateObject para @State simples; views downstream que precisam de um handle bidirecional usam @Bindable em vez de @ObservedObject.
@Environment lê valores injetados por dependência da cadeia de environment. O SwiftUI fornece chaves de environment embutidas (locale, color scheme, dismiss action); apps adicionam chaves customizadas para injeção de dependência específica do domínio.
A camada de property wrapper é o que permite que o body de uma view seja re-executado quando o estado muda. O SwiftUI rastreia leituras dentro de body por meio de dois mecanismos distintos: AttributeGraph (o grafo de dependência privado da Apple que sustenta @State, @Binding e @Environment) para o caminho mais antigo de property wrapper, e o framework Observation da biblioteca padrão (withObservationTracking, público no iOS 17+) para tipos @Observable.8 Quando uma propriedade rastreada é mutada, os bodies correspondentes são re-executados e a maquinaria de diffing computa a mudança mínima da árvore de views.
As duas metades (a camada de árvore de views e a camada de estado) são fracamente acopladas. A árvore de views é por valor e rápida de recomputar. A camada de estado é por referência (para @Observable) ou por valor com ponteiro de armazenamento (para @State) e rastreia leituras. Juntas, elas produzem o modelo do framework “descreva o que deve estar na tela como função do estado, e o framework descobre o diff”.
O que você agora reconhece em mensagens de erro
Lendo mensagens de erro do compilador SwiftUI com o substrato visível:
“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.” Duas instruções de return com tipos concretos diferentes em uma função some View. Solução: use @ViewBuilder para que o result builder embrulhe ambos em _ConditionalContent, ou embrulhe ambos os returns em AnyView.
“The compiler is unable to type-check this expression in reasonable time.” Cadeias longas de body com muitos modificadores esgotam o type checker. Solução: quebre o body em propriedades computadas menores ou subviews; cada peça que retorna some View simplifica o trabalho de inferência.
“Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.” Uma função que esperava uma view recebeu o resultado de um body com múltiplas declarações sem @ViewBuilder. Solução: adicione @ViewBuilder ao parâmetro de closure que aceita o conteúdo com múltiplas declarações.
“Generic parameter ‘Content’ could not be inferred.” Um container customizado recebe @ViewBuilder content: () -> Content e o call site tem uma closure vazia. Solução: result builders precisam de pelo menos uma expressão para inferir Content; closures vazias caem de volta para EmptyView() se o call site explicitamente fornecer isso.
As mensagens de erro são pouco amigáveis porque o substrato é invisível. Lê-las com o substrato visível transforma a maioria delas em “ah, o result builder não consegue transformar isso” ou “ah, eu preciso ou de branching ou de AnyView.”
Quando recorrer para fora do substrato
Alguns padrões que o substrato não trata de forma limpa:
Tipos concretos variádicos. Uma função que retorna um tipo de View diferente por ramo e que você não consegue embrulhar em branching de result builder precisa de AnyView. Aceite o custo (diffing perdido, sem animação) e documente o call site.
Views condicionais multiplataforma. #if os(iOS) em tempo de compilação funciona dentro de um body @ViewBuilder, mas limita a contagem de branching do result builder; bodies condicionais multi-OS às vezes esbarram no limite “expression too complex”. A solução é extrair subviews por plataforma em funções separadas, cada uma retornando some View.
Construção imperativa de views. O framework espera que views sejam expressões, não objetos construídos-e-depois-mutados. O estilo UIKit “criar a label, definir o texto, adicionar à subview” não traduz; o equivalente em SwiftUI é um Text("...") por valor retornado de um body. Padrões que exigem construção imperativa geralmente são sinal de que o trabalho pertence a uma ponte UIViewRepresentable para o UIKit.
O que o padrão significa para apps lançando no iOS 26+
Três conclusões.
-
SwiftUI é Swift, não mágica. Result builders, tipos de retorno opacos e property wrappers estão todos na referência da linguagem Swift. Ler o framework como código Swift, não como uma DSL especial, torna previsíveis as partes surpreendentes.
-
some VieweAnyViewresolvem problemas diferentes. Tipos de retorno opacos são o padrão; type erasure é a saída de emergência. Recorrer aAnyViewdeve ser o caso raro; recorrer asome Viewmais branching de result builder deve ser o comum. -
Result builders são a DSL inteira. Em qualquer lugar onde uma função ou parâmetro seja
@ViewBuilder, a sintaxe sem vírgulas, em formato de declaração, fica disponível. Escrever suas próprias container views com a mesma ergonomia deVStacké um atributo e um parâmetro de closure.
Combine este post com a série de shipped-code do cluster: SwiftUI multiplataforma (Return roda em cinco plataformas com um core SwiftUI compartilhado); a camada visual Liquid Glass; a máquina de estados de Live Activities no iOS; o contrato de runtime do watchOS no Apple Watch. O hub está na Série Apple Ecosystem. Para contexto mais amplo de iOS com agentes de IA, veja o guia de Desenvolvimento de Agentes iOS.
FAQ
O que é o protocolo View no SwiftUI?
O protocolo View tem um requisito: var body: some View { get }. Toda view do SwiftUI é um tipo por valor do Swift em conformidade com View, com uma propriedade computada body que retorna outra view (ou Never para primitive views como Text, Color, Image, EmptyView). O body é anotado com @ViewBuilder para que possa usar a sintaxe DSL sem vírgulas do SwiftUI.
O que some View significa?
some View é um tipo de retorno opaco (Swift 5.1+). O compilador conhece o tipo concreto; o chamador vê apenas a testemunha de protocolo. Tipos opacos permitem que bodies de view retornem tipos complexos como VStack<TupleView<(Text, Image, Spacer)>> sem soletrá-los, preservando a otimização em tempo de compilação. some View é um tipo específico, mesmo que esse tipo não seja visível no call site.
Quando devo usar AnyView?
Use AnyView apenas quando nem branching com result builder @ViewBuilder (if, switch, for) nem generics parametrizados resolvem o problema. Quando o tipo concreto embrulhado muda entre renderizações, a hierarquia de view existente é destruída e uma nova é criada em seu lugar; esse é o momento em que animações reiniciam e o estado da view é zerado. Reembrulhar o mesmo tipo concreto não dispara essa destruição, mas o diffing baseado em tipo estático que o framework prefere também não fica disponível de qualquer forma. Se você se pega recorrendo a AnyView com frequência, o padrão que precisa mudar está upstream: prefira views parametrizadas ou empurre o condicional para dentro de um body de result builder.
O que é @ViewBuilder e onde posso usá-lo?
@ViewBuilder é um result builder (recurso da linguagem Swift). Ele transforma uma closure com múltiplas expressões em um único valor de retorno inserindo chamadas sintetizadas pelo compilador a buildBlock, buildEither, buildOptional, etc. O body de toda view do SwiftUI é @ViewBuilder por padrão. Você pode aplicar @ViewBuilder a qualquer função ou parâmetro de closure para dar aos chamadores a mesma sintaxe DSL; VStack, Card e Section usam o mesmo padrão para aceitar múltiplos filhos.
Por que o body da minha view é re-renderizado quando eu não esperava?
O SwiftUI re-executa body sempre que qualquer propriedade de estado que o body lê é mutada. Property wrappers (@State, @Binding, @Observable, @Environment) rastreiam leituras e disparam re-renderizações em escritas. Re-renderizações inesperadas geralmente vêm de uma mudança no estado de uma view pai, uma mudança em um valor de environment ou uma propriedade lida de um objeto @Observable sendo modificada. O diffing do framework então computa a mudança mínima da árvore.
Referências
-
Apple Developer, “View” e “Configuring views”. O protocolo
View, o tipo associadoBodye o atributo@ViewBuilderembody. ↩ -
Swift Evolution, “SE-0289: Result builders”. A proposta da linguagem que formalizou os result builders (introduzidos como
_functionBuilderno 5.1, formalizados como@resultBuilderno 5.4). DefinebuildBlock,buildEither,buildOptional,buildArray,buildExpression,buildFinalResulte companhia. ↩↩↩ -
Apple Developer, “ViewBuilder” e “ForEach”. O tipo de result builder que o SwiftUI usa para bodies de view (
buildBlockcom variadic generics,buildEither, desempacotamento de opcional).ViewBuildernão expõebuildArray, entãoForEaché a primitiva de iteração para repetir uma view sobre uma coleção. ↩ -
Swift Evolution, “SE-0244: Opaque result types”. A palavra-chave
somepara tipos de retorno opacos, adicionada no Swift 5.1. ↩ -
Apple Developer, “AnyView”. Wrapper de view com type erasure, construção e o trade-off de diffing. ↩
-
Apple Developer, “Group”, “EmptyView” e “TupleView”. Tipos de implementação que os result builders sintetizam. ↩
-
Apple Developer, “State and Data Flow”. A camada de property wrapper:
@State,@Binding,@Observable,@Environment. O sistema de observação do SwiftUI e a macro@Observableno iOS 17+. ↩ -
Apple Developer, “Observation” e “Migrating from the Observable Object protocol to the Observable macro”. O framework Observation da biblioteca padrão, incluindo
withObservationTracking(_:onChange:), mais o caminho de migração no iOS 17 deObservableObjectpara@Observable. ↩