Communitygithub.com

gtokman/skills

Como o SwiftUI decide o que renderizar novamente — identidade, tempo de vida, dependências — e como escrever views que atualizam eficientemente, especialmente SwiftUI hospedado em células UIKit (UIHostingConfiguration) e views reconstruídas a partir de conteúdo gerado ou transmitido. Use ao escrever ou revisar views SwiftUI para desempenho, corrigir views que perdem estado, piscam, renderizam novamente com muita frequência ou recarregam imagens, escolher identificadores ForEach, hospedar SwiftUI em células de collection/table view, ou depurar layout que muda quando views são encapsuladas.

O que é skills?

Este skill fornece um guia completo para entender e otimizar o ciclo de renderização do SwiftUI. Ele explica os três pilares fundamentais — identidade, tempo de vida e dependências — que determinam quando e por que uma view é atualizada. O conteúdo abrange desde regras básicas, como usar identificadores estáveis e únicos em ForEach e evitar AnyView, até técnicas avançadas para otimizar o corpo de uma view, como evitar trabalho pesado em `body` e usar `EquatableView` criteriosamente. Inclui seções específicas para SwiftUI hospedado em células UIKit com UIHostingConfiguration, onde o gerenciamento de estado e reutilização de células é crítico. Um checklist de revisão e métodos de verificação (como `_printChanges()` e Instruments) ajudam a depurar problemas de desempenho. Baseado em sessões da WWDC21 e WWDC23, combinado com lições práticas verificadas em projetos reais, este skill é essencial para qualquer desenvolvedor que queira escrever SwiftUI eficiente e sem bugs de renderização.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/gtokman/skills/tree/HEAD/swiftui-rendering

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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 / @StateObject está vinculado a essa identidade: quando a identidade muda, o estado é substituído e onAppear/task executam 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 um if/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 @Observable que seu body lê. Quando uma muda, apenas as views que dependem dela recebem um novo body. 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 → body executado → filhos comparados → recursão.

Regras

Identidade

  1. Nunca identifique por índice (ForEach(Array(x.enumerated()), id: \.offset), 0..<count sobre 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.
  2. IDs devem ser estáveis e únicos. Sem UUID() em um id computado (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 um ForEach se comportam mal.
  3. 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.
  4. 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, frame sem tamanhos, layoutPriority(0), transformEnvironment) custam quase nada. Use uma ramificação apenas para views genuinamente diferentes.
  5. Alterne layouts com AnyLayout, não com if wrap { FlowLayout { … } } else { HStack { … } } — os filhos mantêm sua identidade e estado.
  6. Evite AnyView. Ele esconde a estrutura de tipos do SwiftUI (pior diffing, e em List/Table força a construir cada linha para contá-las). Use funções @ViewBuilder, switch, genéricos.
  7. 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

  1. Passe para as views apenas o que elas usam. Uma view que recebe um modelo inteiro executa body novamente quando qualquer campo muda; passe a imagem, não o cachorro. Extraia subviews para escopar dependências — com discernimento, não para cada linha.
  2. @Observable rastreia o que body lê. Ler uma propriedade em um closure de ação não cria dependência; lê-la em body cria. Coloque o estado que a view mostra no modelo e leia-o em body apenas onde necessário.
  3. Nunca coloque um novo closure (ou outro valor não comparável) no ambiente por renderização — por exemplo, .environment(\.actions, Actions { … }) em um body. 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.
  4. Não notifique quando nada mudou. Uma propriedade @Observable notifica em cada set. Proteja: if new != value { value = new } — importa quando você reconstrói estado derivado a cada chunk ou tick.
  5. Views Equatable para 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 o layoutPriority do filho de seu contêiner. Dentro de um HStack/linha, um filho layoutPriority(1) encapsulado em EquatableView para 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

  1. 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 (um NSCache chaveado por entrada), ou eleve-o para a view cujas entradas realmente mudam.
  2. Separe entradas caras de estado que muda rapidamente. Se uma view decodifica dados e mantém @State que 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.
  3. Trabalho lento vai em .task, não em init ou body. Torne-o async; nunca bloqueie no valor inicial de uma propriedade dinâmica.
  4. Armazene em cache formatação custosa — AttributedString(markdown:), interpolação de strings em caminhos quentes, consultas de bundle.

Listas

  1. List/Table coleta todos os IDs antecipadamente. Mantenha a geração de IDs barata e dê a cada elemento do ForEach um número constante de views — sem filtros if ou AnyView dentro do closure da linha (filtre os dados, no modelo, não inline em body). ForEach aninhado é 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 Equatable no 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 pequeno Layout personalizado (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 .aspectRatio apenas quando você tem uma proporção.

Checklist de revisão

  • Todo ForEach tem IDs estáveis e únicos — sem \.offset, sem UUID() computado.
  • Sem AnyView; ramificações apenas para views genuinamente diferentes; AnyLayout para alternâncias de layout.
  • Views recebem apenas os dados que renderizam.
  • Nada não comparável é criado por renderização no ambiente.
  • body não faz parsing/decodificação/filtragem; entradas caras separadas de @State rápido.
  • Propriedades observáveis não são redefinidas para valores iguais.
  • .equatable() onde economiza trabalho — e não em filhos que usam layoutPriority.
  • 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 um body registra 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:), proposedSize com altura nula) e compare PNGs. Isso captura regressões de layout como a do EquatableView/layoutPriority. (Map, ScrollView e 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).

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Habilidades Relacionadas