Renderização SwiftUI
O SwiftUI observa três coisas no seu código: identidade (é o mesmo elemento de antes?), tempo de vida (quanto tempo essa identidade dura — e com ela, @State), e dependências (quais entradas, quando mudam, fazem um body ser executado novamente). Quase toda pergunta "por que re-renderizou / perdeu estado / animou estranhamente / travou" é uma destas.
O modelo
- Valores de view são efêmeros; identidade não é. Uma struct de view é criada, comparada com o valor anterior e descartada. O que persiste é sua identidade, e o armazenamento de
@State/@StateObjectestá vinculado a essa identidade: quando a identidade muda, o estado é substituído eonAppear/taskexecutam novamente, imagens recarregam. - Dois tipos de identidade. Explícita: IDs do
ForEach,Identifiable,.id(_:). Estrutural: o tipo e a posição de uma view na hierarquia — cada ramo de umif/switché uma identidade diferente. - O grafo de dependências. As dependências de uma view são suas propriedades armazenadas (entradas de seu pai),
@State,@Environment,@Binding, e as propriedades@Observableque seubodylê. Quando uma muda, apenas as views que dependem dela recebem um novobody. O SwiftUI então compara os novos valores filhos com os antigos e para onde nada mudou. - Ordem de atualização para uma view invalidada: novo valor → propriedades dinâmicas atualizadas →
bodyexecutado → filhos comparados → recursão.
Regras
Identidade
- Nunca identifique por índice (
ForEach(Array(x.enumerated()), id: \.offset),0..<countsobre dados dinâmicos). Qualquer coisa inserida acima desloca todos os índices posteriores: seu estado é reiniciado, suas imagens recarregam, animações vão para o lugar errado. Use um ID estável e único. - IDs devem ser estáveis e únicos. Sem
UUID()em umidcomputado (nova identidade a cada renderização). Sem nomes que podem se repetir (duas guloseimas "Biscoito de cachorro"). Torne repetições únicas (chave,chave#2) se os dados não puderem garantir — IDs duplicados em umForEachse comportam mal. - Para conteúdo gerado ou transmitido, derive a identidade da estrutura que não se move: onde o elemento começa no texto de origem (streaming apenas anexa), ou posição do template + chave por iteração para saída de programa (modelo do React:
template:key). Nomeie itens sem chave (texto solto, parágrafos) após o elemento chaveado antes deles mais um contador (e812+2), para que novo conteúdo no final nunca renomeie itens anteriores. - Prefira uma view com modificadores mutáveis a ramificações.
if expired { v.opacity(0.3) } else { v }são duas identidades;v.opacity(expired ? 0.3 : 1)é uma. Valores "inertes" (opacidade 1, padding 0,framesem tamanhos,layoutPriority(0),transformEnvironment) custam quase nada. Use uma ramificação apenas para views genuinamente diferentes. - Alterne layouts com
AnyLayout, não comif wrap { FlowLayout { … } } else { HStack { … } }— os filhos mantêm sua identidade e estado. - Evite
AnyView. Ele esconde a estrutura de tipos do SwiftUI (pior diffing, e emList/Tableforça a construir cada linha para contá-las). Use funções@ViewBuilder,switch, genéricos. - Modele tipos de elemento como um enum e faça switch sobre ele na view de despacho, sem
default: um novo caso não pode ser ignorado silenciosamente.
Dependências
- Passe para as views apenas o que elas usam. Uma view que recebe um modelo inteiro executa
bodynovamente quando qualquer campo muda; passe a imagem, não o cachorro. Extraia subviews para escopar dependências — com discernimento, não para cada linha. @Observablerastreia o quebodylê. Ler uma propriedade em um closure de ação não cria dependência; lê-la embodycria. Coloque o estado que a view mostra no modelo e leia-o embodyapenas onde necessário.- Nunca coloque um novo closure (ou outro valor não comparável) no ambiente por renderização — por exemplo,
.environment(\.actions, Actions { … })em umbody. Ele não pode ser comparado, então todo leitor é invalidado a cada atualização do pai. Coloque um objeto estável no ambiente (.environment(model)com@Observable, leia como@Environment(Model.self) var model: Model?), e chame-o a partir das ações. - Não notifique quando nada mudou. Uma propriedade
@Observablenotifica em cadaset. Proteja:if new != value { value = new }— importa quando você reconstrói estado derivado a cada chunk ou tick. - Views
Equatablepara pular subárvores inalteradas. Conforme a view (apenas possível quando não tem property wrappers) e aplique.equatable()onde um pai re-renderiza frequentemente com filhos principalmente inalterados (uma lista de blocos enquanto o conteúdo é transmitido). Escreva um==personalizado comparando apenas o que a view mostra. ⚠️.equatable()esconde olayoutPrioritydo filho de seu contêiner. Dentro de umHStack/linha, um filholayoutPriority(1)encapsulado emEquatableViewpara de ganhar espaço extra e a linha se comporta de forma diferente. Use.equatable()em listas verticais de blocos, não em filhos de linhas/stacks que dependem de prioridade.
body barato
- Nenhum parsing, decodificação, filtragem, ordenação ou trabalho pesado de alocação em
body. Ele executa a cada invalidação. Pré-calcule no modelo, cache (umNSCachechaveado por entrada), ou eleve-o para a view cujas entradas realmente mudam. - Separe entradas caras de estado que muda rapidamente. Se uma view decodifica dados e mantém
@Stateque muda frequentemente (arrastar, escanear, pairar), a decodificação re-executa a cada mudança. Decodifique em um pai, passe o resultado para um filho que possui o@State. - Trabalho lento vai em
.task, não eminitoubody. Torne-oasync; nunca bloqueie no valor inicial de uma propriedade dinâmica. - Armazene em cache formatação custosa —
AttributedString(markdown:), interpolação de strings em caminhos quentes, consultas de bundle.
Listas
List/Tablecoleta todos os IDs antecipadamente. Mantenha a geração de IDs barata e dê a cada elemento doForEachum número constante de views — sem filtrosifouAnyViewdentro do closure da linha (filtre os dados, no modelo, não inline embody).ForEachaninhado é aceitável para seções.
SwiftUI em células UIKit (UIHostingConfiguration)
- Células são reutilizadas, e o estado da view SwiftUI pode ser carregado para o próximo item mostrado naquela célula. Dê à raiz
.id(item.id)para que um item diferente receba uma árvore nova — e mantenha o estado visível ao usuário (marcado, expandido, seleção) no modelo, chaveado por IDs estáveis, não em@State, ou será perdido/aplicado incorretamente na reutilização e rolagem. - Reconfigurar uma célula reconstrói o valor da view raiz. Se o conteúdo é impulsionado por um modelo observado, torne a view raiz
Equatableno que ela mostra (não em entradas brutas que apenas alimentam o modelo) e aplique.equatable(), para que reconfiguração por atualização não re-execute toda a árvore — a observação faz a atualização real. - Auto-dimensionamento mede com altura aberta. Views sem um tamanho ideal colapsam para zero lá:
Canvas, formas,GeometryReader..aspectRatio(r, contentMode: .fit)não é suficiente — dê a elas um tamanho com um pequenoLayoutpersonalizado (largura da proposta, altura = largura / proporção; ignore alturas de sondagem). .aspectRatio(nil, contentMode:)prende uma view ao tamanho ideal de seu conteúdo — cartões param de preencher sua linha. Aplique.aspectRatioapenas quando você tem uma proporção.
Checklist de revisão
- Todo
ForEachtem IDs estáveis e únicos — sem\.offset, semUUID()computado. - Sem
AnyView; ramificações apenas para views genuinamente diferentes;AnyLayoutpara alternâncias de layout. - Views recebem apenas os dados que renderizam.
- Nada não comparável é criado por renderização no ambiente.
-
bodynão faz parsing/decodificação/filtragem; entradas caras separadas de@Staterápido. - Propriedades observáveis não são redefinidas para valores iguais.
-
.equatable()onde economiza trabalho — e não em filhos que usamlayoutPriority. - Células hospedadas: raiz
.id(item.id), estado visível no modelo, raiz Equatable. - Views sem um tamanho ideal recebem um antes de entrar em células de auto-dimensionamento.
Verificando
let _ = Self._printChanges()no topo de umbodyregistra por que ele foi executado (@self,@identity, ou a propriedade que mudou). Apenas depuração — remova antes de enviar.- Instruments → template SwiftUI para contagens de body e atualizações lentas; Hangs/Hitches para os efeitos.
- Teste de identidade: renderize cada prefixo de conteúdo transmitido e afirme que todo ID de item completo aparece inalterado na renderização final; após interações, afirme que elementos sobreviventes mantiveram seus IDs; afirme que não há IDs irmãos duplicados.
- Diff de pixel antes/depois de uma refatoração de renderização: renderize views fora da tela no macOS com
ImageRenderer(mesmo código,.frame(width:),proposedSizecom altura nula) e compare PNGs. Isso captura regressões de layout como a doEquatableView/layoutPriority. (Map,ScrollViewe imagens remotas não renderizam fora da tela; compare todo o resto.) Faça bissecção de uma diferença alternando um arquivo alterado por vez.
Referências
references/case-study.md— o renderizador de resposta AddToChatMenu (marcação gerada por modelo transmitida em células de collection view): cada problema encontrado contra estas regras e sua correção.- WWDC21 Demystify SwiftUI (sessão 10022); WWDC23 Demystify SwiftUI performance (sessão 10160).