O que Especialista em SwiftUI faz?
Uma tela SwiftUI vive ou morre pela propriedade de estado: coloque o estado no lugar errado e cada tecla reavalia toda a árvore, listas gaguejam e animações rasgam. Este skill constrói e revisa visualizações SwiftUI para que a invalidação permaneça escopada às visualizações que realmente leem dados alterados, o orçamento de quadros se mantenha durante a rolagem e o resultado seja acessível por padrão, em vez de adaptado posteriormente.
Procedimento operacional
Trabalhe nesta ordem porque o design do estado determina tudo a jusante — consertar o estado depois que a hierarquia da visualização estiver construída significa reescrever a hierarquia.
Etapa 1: Coletar entradas
Colete antes de escrever qualquer código de visualização. Onde o usuário não souber, use o padrão e rotule como um palpite.
- Dispositivos alvo e taxa de atualização (padrão: assumir iPhone ProMotion 120Hz mais um dispositivo de linha de base 60Hz).
- Forma dos dados: quantos itens na maior lista, com que frequência os dados mudam, se eles chegam em streaming.
- Mapa de propriedade: para cada parte do estado, qual visualização a cria, quais visualizações a leem, quais a alteram.
- Requisitos de acessibilidade além do básico (padrão: suporte completo a Dynamic Type + VoiceOver).
Etapa 2: Escolher a propriedade de estado por valor
Escolha o wrapper mais restrito que funciona:
@State— estado local, de propriedade da visualização, tipo valor. Também o proprietário correto para uma instância de modelo@Observablecriada pela visualização.@Binding— uma referência bidirecional ao estado possuído em outro lugar. Passe uma ligação apenas quando o filho alterar; passe um valor simples quando ele apenas ler.@Observable(framework Observation) — objetos de modelo; as visualizações observam apenas as propriedades que leem, então a invalidação é por propriedade, não por objeto.@Environment— injeção de dependência pela árvore para serviços e modelos compartilhados.
Regra: o estado reside no ancestral comum mais baixo de todas as visualizações que o leem ou escrevem — nem mais baixo (ele é redefinido quando a visualização desaparece) nem mais alto (ele amplia a invalidação).
Etapa 3: Compor visualizações para escopar a invalidação
- Mantenha
bodypequeno; extraia subvisualizações para que uma alteração invalide uma linha, não a tela. Umbodyque exceda aproximadamente 50 linhas ou três níveis de aninhamento é candidato a divisão. - Passe dados mínimos para subvisualizações — passe
item.title, não o repositório inteiro — para que apenas as visualizações afetadas sejam renderizadas novamente. - Use auxiliares
@ViewBuilderem vez de closures aninhadas gigantes. - Nunca faça trabalho pesado (formatação, classificação, filtragem, decodificação) em
body; calcule-o no modelo e entregue valores prontos à visualização.bodydeve permanecer na faixa de microssegundos porque pode ser executado a cada quadro durante a animação.
Etapa 4: Listas e o orçamento de quadros
O orçamento é de 8,3 ms por quadro a 120 Hz, 16,7 ms a 60 Hz — tudo na thread principal para aquele quadro, não apenas sua linha. Regras práticas:
- Use
ListouLazyVStackpara qualquer coisa acima de ~50 linhas para que as linhas sejam renderizadas sob demanda; umVStacksimples em umScrollViewconstrói todas as linhas antecipadamente. - Dê aos itens
ids estáveis de seus dados (item.id), nunca índices de array ouUUID()calculado embody— ids instáveis destroem a identidade da linha, causando cintilação, perda de posição de rolagem e perda de estado da linha. - As linhas devem ter custo constante: sem formatadores de data por linha ou decodificação de imagem; armazene em cache formatadores, use
AsyncImageou um pipeline de imagem com cache. - Verifique com o template SwiftUI do Instruments: observe as contagens de avaliação do corpo da visualização durante a rolagem. Um tipo de linha cujo corpo é avaliado quando um estado não relacionado muda é um bug de propriedade de estado — corrija a Etapa 2, não use memorização ao redor dela.
Etapa 5: Animar a partir do estado
- Direcione a animação a partir do estado:
withAnimation { model.expanded.toggle() }. - Prefira
.animation(_:value:)escopado a um valor específico em vez de animações globais implícitas, que animam coisas que você não pretendia. - Use
matchedGeometryEffectpara transições de elemento compartilhado;transitionpara inserir/remover. - Respeite
accessibilityReduceMotion: substitua movimento por fades de opacidade quando estiver definido.
Etapa 6: Passagem de acessibilidade
- Adicione
accessibilityLabel/accessibilityValue; agrupe elementos relacionados comaccessibilityElement(children: .combine)para que uma linha seja lida como uma frase, não cinco fragmentos. - Respeite o Dynamic Type — estilos de texto do sistema, sem quadros fixos que cortem o texto; verifique no maior tamanho de acessibilidade.
- Verifique o contraste e adicione variantes
#Previewpara modo escuro, Dynamic Type grande e estados vazio/carregando/erro com dados de amostra.
Artefato trabalhado: propriedade de estado, ruim vs bom
Ruim — a tela inteira é invalidada a cada tecla porque o texto de pesquisa reside em um modelo que toda a árvore observa, e as linhas recebem o repositório inteiro:
@Observable final class ScreenStore {
var searchText = ""
var items: [Item] = []
}
struct ContactList: View {
@State private var store = ScreenStore()
var body: some View {
VStack {
TextField("Search", text: $store.searchText)
ForEach(store.items) { item in
ContactRow(store: store, item: item) // linha observa tudo
}
}
}
}
Bom — o texto de pesquisa é local da visualização, as linhas recebem apenas os valores que leem, e a lista é lazy com ids estáveis:
struct ContactList: View {
@State private var searchText = ""
@Environment(ContactModel.self) private var model
var body: some View {
List(model.filtered(searchText)) { item in
ContactRow(name: item.name, isOnline: item.isOnline)
}
.searchable(text: $searchText)
}
}
struct ContactRow: View {
let name: String
let isOnline: Bool
var body: some View {
Label(name, systemImage: isOnline ? "circle.fill" : "circle")
}
}
Digitar na versão boa invalida o fechamento de conteúdo List e as linhas cuja associação filtrada mudou — nada mais.
Entregável
Produza código de visualização SwiftUI funcional mais um parágrafo de mapa de estado indicando, para cada parte do estado: seu proprietário, seu wrapper e quais visualizações leem vs escrevem. Para revisões, produza uma lista de descobertas ordenadas por impacto no orçamento de quadros, cada uma com o conserto.
NÃO FAÇA
- Nunca altere o estado durante a avaliação de
body— isso causará loops ou acionará avisos de comportamento indefinido. - Não faça trabalho assíncrono em
onAppearcom umaTaskbruta — use.task {}, que cancela automaticamente ao desaparecer. - Não coloque tipos de visualização (
Color,Image,AnyView) em modelos; os modelos devem permanecer testáveis sem interface do usuário. - Não eleve todo o estado a um repositório de nível de tela "por simplicidade" — esse é exatamente o padrão que faz cada tecla redesenhar a tela.
- Não encubra bugs de invalidação com
equatable()ou cache antes de corrigir a propriedade; o bug ressurgirá na próxima visualização que observar o mesmo objeto. - Não use quadros fixos em texto — o Dynamic Type o cortará.
Nível de qualidade
Uma visualização finalizada é aprovada quando:
- Cada parte do estado tem exatamente um proprietário e o wrapper mais restrito que funciona.
- O Instruments mostra corpos de linha sendo avaliados apenas quando seus próprios dados mudam.
- Rolar a maior lista esperada mantém o orçamento de quadros no dispositivo de linha de base de 60 Hz.
- O estado de longa duração é elevado acima de qualquer visualização que possa desaparecer, para que sobreviva à navegação.
- O tratamento de teclado e áreas seguras é feito (
safeAreaInset,scrollDismissesKeyboard). - O VoiceOver lê cada elemento interativo com um rótulo significativo; o layout sobrevive ao maior tamanho de Dynamic Type;
reduceMotioné respeitado. - Existem visualizações para estados de carregamento, vazio, erro, modo escuro e tipo grande.
Para perfilagem em todo o app além de uma única tela, combine com mobile-perf-profiler.