← Todos os Posts

Do que o SwiftUI é feito

O SwiftUI se apoia em três recursos da linguagem Swift: result builders, tipos de retorno opacos e uma árvore de views com tipo por valor. Quando o substrato fica visível, as partes do SwiftUI que surpreendem os 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 normal. Se não entende, o framework parece mágica que ocasionalmente morde.

Este post percorre o substrato. Não há nenhum LiveActivityManager aqui, nenhum screenshot do Get Bananas. O ponto é o framework, não um projeto; quando o framework fica legível, todo post sobre código já entregue do cluster fica mais limpo de ler.

TL;DR

  • Uma view do SwiftUI é um tipo por valor do Swift em conformidade com View. O protocolo tem um único requisito: var body: some View { get }. Todo o resto é construído sobre recursos da linguagem Swift.
  • @ViewBuilder é um result builder. O body de toda View é 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; quem chama, não. O tipo opaco é o que torna os bodies de view rápidos em tempo de compilação e em tempo de execução; AnyView é a saída de emergência com type erasure para os casos em que a opacidade não funciona.
  • Group, EmptyView, TupleView, _ConditionalContent sã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 único 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 composável. Toda View retorna outra View a partir do seu body, e assim por diante, até a cadeia terminar em uma das views primitivas do framework (como Text, Color, Image, EmptyView) cujo Body é Never. As views primitivas são as folhas da árvore; as views que você escreve são os galhos.

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 a uma closure com uma sequência de expressões ser 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 do body de uma view do SwiftUI.

O formato do protocolo é incomum por duas razões.

Primeiro, o requisito é uma propriedade computada, não um método. O body da view é recomputado a cada passagem de render quando o SwiftUI decide que o estado da view mudou. O framework trata body como algo barato de chamar; computações longas dentro de body são um anti-padrão porque rodam a cada render.

Segundo, Self.Body é associado, não apagado. O tipo concreto do body de uma view faz parte da 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 com 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. No 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, é mais ou menos isto:

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 antigas do SwiftUI traziam um conjunto fixo de overloads de buildBlock para 1, 2, 3, … até 10 filhos; o SwiftUI atual usa o suporte a generics variádicos do Swift (buildBlock<each Content>), de modo que um body com onze ou mais views irmãs já não é mais um caso especial.

O mesmo padrão lida com fluxo de controle. O body de uma view com uma declaração if fica assim:

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 do resultado em tempo de compilação, mesmo que apenas um dos ramos seja executado em qualquer render dado.

if let, switch, desempacotamento de opcionais e algumas outras construções são tratados pelos vários métodos estáticos buildXxx do result builder.3 Conteúdo repetido é o caso notável que o recurso da linguagem suporta via buildArray, mas @ViewBuilder não: um for cru dentro de um body falha com “closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.” A resposta no estilo SwiftUI é ForEach, que recebe um RandomAccessCollection e uma closure de conteúdo e sintetiza a iteração como uma única view com tipo por valor. A DSL não é sob medida; são os 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 significa 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 vou dizer qual.” O compilador rastreia o tipo concreto internamente para otimização; quem chama sua view enxerga apenas a witness do protocolo. O padrão é o que permite ao body de uma view retornar um tipo complexo como VStack<TupleView<(Text, Image, Spacer)>> sem que você tenha de escrever 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 só. Result builders são o que faz formas de retorno condicionais funcionarem; 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, oculto); any View é existencial (uma caixa que pode conter qualquer tipo conforme, com sobrecarga em runtime). Uma função livre ou uma propriedade pode legalmente retornar any View. O body do protocolo View, contudo, não pode: o protocolo exige associatedtype Body: View, e any View não está, ele próprio, 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 variantes 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 a partir 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 aceita qualquer view conforme, e o SwiftUI o aceita onde uma View é esperada.5

O caso de uso da 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 do 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 renders, 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. Re-embrulhar o mesmo tipo concreto não dispara essa destruição, mas o diffing dirigido por tipo estático que o framework prefere também deixa de estar disponível de qualquer jeito.

A regra correta é: 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 é normalmente 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 específicos de view. 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 próprio container não adiciona estrutura visual; o conteúdo renderiza exatamente como renderizaria individualmente. O caso de uso é embrulhar várias 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 o ramo falso de uma condicional quando um if não tem else. Retornar EmptyView() a partir 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 várias 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 underscore inicial) é o tipo que lida com ramos if/else. O tipo aparece na superfície pública de ViewBuilder, mas o nome com underscore sinaliza “não escreva contra isso 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 receber a anotação @ViewBuilder, e a mesma sintaxe da DSL passa a ser válida dentro dela.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")
}

O padrão é como o próprio SwiftUI faz VStack, HStack, ZStack, List, Form, Section, Group e NavigationStack aceitarem 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 views container com a mesma ergonomia das do framework, sem nenhum 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 corpo da closure que quem chama 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 é estado, e essa metade é construída sobre os property wrappers do Swift.7

Os property wrappers mais relevantes para a autoria de views:

@State detém uma porção de estado com tipo por valor dentro de uma única view. Ler a propriedade lê o storage subjacente; atribuir a ela dispara um re-render da view. O wrapper é apropriado para estado simples, local à view (o on/off de um toggle, a string de rascunho de um campo de texto).

@Binding é uma referência bidirecional para o 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 antigo de conformidade com ObservableObject. A macro aplicada a uma classe gera o tracking do framework Observation, de modo que propriedades da classe disparam re-renders 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 um @State simples; views downstream que precisam de um handle bidirecional usam @Bindable em vez de @ObservedObject.

@Environment lê valores injetados como dependência da cadeia de ambiente. O SwiftUI fornece chaves de ambiente embutidas (locale, esquema de cor, ação de dismiss); apps adicionam chaves customizadas para injeção de dependência específica do domínio.

A camada de property wrappers é 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 privado de dependências da Apple que sustenta @State, @Binding e @Environment) para o caminho mais antigo de property wrappers, 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 alteração mínima na árvore de views.

As duas metades (a camada da árvore de views e a camada de estado) são fracamente acopladas. A árvore de views tem tipo por valor e é rápida de recomputar. A camada de estado tem tipo por referência (para @Observable) ou tipo por valor com ponteiro de storage (para @State) e rastreia leituras. Juntas, elas produzem o modelo do framework: “descreva o que deve estar na tela como uma função do estado, e o framework calcula o diff.”

O que você passa a reconhecer nas mensagens de erro

Lendo erros de compilação do 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 declarações de return com tipos concretos diferentes em uma função some View. Correção: use @ViewBuilder para que o result builder envolva 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. Correção: quebre o body em propriedades computadas menores ou em sub-views; cada pedaço 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. Correção: adicione @ViewBuilder ao parâmetro de closure que aceita o conteúdo de múltiplas declarações.

“Generic parameter ‘Content’ could not be inferred.” Um container customizado recebe @ViewBuilder content: () -> Content e o local de chamada tem uma closure vazia. Correção: result builders precisam de pelo menos uma expressão para inferir Content; closures vazias caem para EmptyView() se o local de chamada o fornecer explicitamente.

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 ir além do substrato

Alguns padrões que o substrato não trata com elegância:

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 local de chamada.

Views condicionais cross-platform. Um #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 de “expression too complex”. A correção é extrair sub-views por plataforma para 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 “crie a label, defina o texto, adicione à subview” não se traduz; o equivalente em SwiftUI é um Text("...") com tipo por valor retornado de um body. Padrões que exigem construção imperativa são geralmente um sinal de que o trabalho pertence a uma ponte UIViewRepresentable para o UIKit.

O que o padrão significa para apps que rodam no iOS 26+

Três conclusões.

  1. 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, e não como uma DSL especial, torna previsíveis as partes surpreendentes.

  2. some View e AnyView resolvem problemas diferentes. Tipos de retorno opacos são o padrão; type erasure é a saída de emergência. Recorrer a AnyView deveria ser o caso raro; recorrer a some View mais branching de result builder deveria ser o comum.

  3. 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, está disponível. Escrever suas próprias views container com a mesma ergonomia de VStack é um atributo e um parâmetro de closure.

Combine este post com a série de código já entregue do cluster: SwiftUI cross-platform shipping (o Return roda em cinco plataformas com um único 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 fica na Apple Ecosystem Series. Para um contexto mais amplo de iOS-com-AI-agents, veja o iOS Agent Development guide.

FAQ

O que é o protocolo View no SwiftUI?

O protocolo View tem um único 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 views primitivas como Text, Color, Image, EmptyView). O body é anotado com @ViewBuilder para que possa usar a sintaxe sem vírgulas da DSL do SwiftUI.

O que significa some View?

some View é um tipo de retorno opaco (Swift 5.1+). O compilador conhece o tipo concreto; quem chama enxerga apenas a witness do 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, embora esse tipo não fique visível no local de chamada.

Quando devo usar AnyView?

Use AnyView apenas quando nem o branching com result builder @ViewBuilder (if, switch, for) nem generics parametrizados resolverem o problema. Quando o tipo concreto embrulhado muda entre renders, a hierarquia de view existente é destruída e uma nova é criada em seu lugar; é nesse momento que animações reiniciam e o estado da view é resetado. Re-embrulhar o mesmo tipo concreto não dispara essa destruição, mas o diffing dirigido por tipo estático que o framework prefere também deixa de estar disponível de qualquer jeito. Se você se vê recorrendo a AnyView com frequência, o padrão que precisa mudar está mais acima: prefira views parametrizadas ou empurre a 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 várias 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 a quem chama a mesma sintaxe da DSL; VStack, Card e Section usam o mesmo padrão para aceitar múltiplos filhos.

Por que o body da minha view re-renderiza 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-renders em escritas. Re-renders inesperados normalmente vêm de uma mudança no estado de uma view pai, de uma mudança em um valor de ambiente, ou de uma propriedade de leitura de um objeto @Observable sendo modificada. O diffing do framework então computa a mudança mínima na árvore.

Referências


  1. Apple Developer, “View” e “Configuring views”. O protocolo View, o tipo associado Body e o atributo @ViewBuilder em body

  2. Swift Evolution, “SE-0289: Result builders”. A proposta de linguagem que formalizou os result builders (introduzidos como _functionBuilder no 5.1, formalizados como @resultBuilder no 5.4). Define buildBlock, buildEither, buildOptional, buildArray, buildExpression, buildFinalResult e companhia. 

  3. Apple Developer, “ViewBuilder” e “ForEach”. O tipo de result builder que o SwiftUI usa para bodies de view (buildBlock com generics variádicos, buildEither, desempacotamento de opcionais). ViewBuilder não expõe buildArray, então ForEach é o primitivo de iteração para repetir uma view sobre uma coleção. 

  4. Swift Evolution, “SE-0244: Opaque result types”. A palavra-chave some para tipos de retorno opacos, adicionada no Swift 5.1. 

  5. Apple Developer, “AnyView”. Wrapper de view com type erasure, construção e o trade-off de diffing. 

  6. Apple Developer, “Group”, “EmptyView” e “TupleView”. Tipos de implementação que os result builders sintetizam. 

  7. Apple Developer, “State and Data Flow”. A camada de property wrappers: @State, @Binding, @Observable, @Environment. O sistema de observação do SwiftUI e a macro @Observable no iOS 17+. 

  8. 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 de ObservableObject para @Observable

Artigos relacionados

O runtime do watchOS é um contrato, não uma tarefa em background

O watchOS não tem um background no estilo do iOS. WKExtendedRuntimeSession é o contrato; sem ele, o app é suspenso quand…

15 min de leitura

Três superfícies: humano, Apple Intelligence, agente

Toda funcionalidade de um app iOS enfrenta três superfícies: humano, Apple Intelligence, agente. Cada uma tem obrigações…

15 min de leitura

A camada de limpeza é o verdadeiro mercado de agentes de IA

A Charlie Labs pivotou de construir agentes para limpar o que eles deixam para trás. O mercado de agentes de IA está sai…

14 min de leitura