Saiba o que são LazyVStack e LazyHStack no SwiftUI — stacks preguiçosos para renderização eficiente de listas roláveis, grades e carrosséis no iOS, macOS, watchOS e tvOS. Ao contrário dos VStack e HStack comuns, os stacks preguiçosos criam elementos apenas quando aparecem na área visível, o que reduz criticamente o consumo de memória ao trabalhar com grandes conjuntos de dados. A arquitetura dos stacks preguiçosos é baseada no protocolo Layout e é integrada com identificação através de ForEach e ScrollView.
Principais pontos
LazyVStack e LazyHStack são contêineres de layout no SwiftUI que criam e exibem visualizações filhas apenas quando necessário, quando se tornam visíveis na área rolável. LazyVStack organiza elementos verticalmente (de cima para baixo), enquanto LazyHStack os organiza horizontalmente (da esquerda para a direita).
Ambos os stacks foram introduzidos pela Apple no SwiftUI 2.0 (iOS 14, macOS 11, watchOS 7, tvOS 14) juntamente com LazyVGrid e LazyHGrid. Antes do surgimento dos stacks preguiçosos, os desenvolvedores tinham que usar UITableView e UICollectionView através de UIViewRepresentable para trabalhar eficientemente com listas grandes. LazyVStack eliminou essa necessidade fornecendo uma interface nativa SwiftUI com carregamento preguiçoso automático.
De acordo com a WWDC Session 10031 da Apple (2020), os stacks preguiçosos usam um mecanismo de criação de visualização adiada: o SwiftUI armazena os dados de origem (por exemplo, uma matriz de modelos) e cria instâncias de visualização imediatamente antes de renderizar na tela. Ao rolar, os stacks reutilizam visualizações já criadas, evitando novas alocações — isso reduz a carga no alocador de memória e no coletor de lixo do Swift.
Para trabalhar com stacks preguiçosos, coloque-os sempre dentro de uma ScrollView — sem rolagem, os elementos que se estendem além dos limites da tela serão simplesmente cortados, não criados preguiçosamente.
O mecanismo de carregamento preguiçoso no LazyVStack é baseado na geometria: o SwiftUI rastreia a posição de cada visualização filha em relação ao contêiner ScrollView. Quando um elemento cruza o limite da área visível (com um pequeno buffer de alguns pontos), o sistema chama seu inicializador e renderiza o conteúdo. Quando um elemento sai da tela, o SwiftUI destrói a visualização mas preserva o estado através de @State se estiver marcado como preservável.
Esta abordagem difere do VStack, onde todas as visualizações filhas são criadas imediatamente na inicialização do contêiner, independentemente da visibilidade. Para uma lista de 10.000 elementos, o VStack criará 10.000 instâncias de visualização na memória, enquanto o LazyVStack criará apenas aquelas que cabem na tela (geralmente 8–15).
LazyVStack aceita três parâmetros de configuração: alignment (HorizontalAlignment — leading, center, trailing), spacing (CGFloat — espaçamento entre elementos), e pinnedViews (PinnedScrollableViews — fixação de cabeçalhos de seção). LazyHStack usa os mesmos parâmetros, mas alignment aceita VerticalAlignment (top, center, bottom).
A principal diferença entre LazyVStack e VStack é a estratégia de criação de elementos filhos. VStack (stack eager) calcula o tamanho e a posição de todas as visualizações filhas no momento da renderização, o que o torna inadequado para listas dinâmicas grandes. LazyVStack (stack lazy) adia a criação até que o elemento se torne visível.
Vamos comparar o comportamento usando uma lista de 1000 linhas de texto. VStack carregará todas as 1000 linhas na memória imediatamente, chamando o inicializador de cada linha e alocando memória para ela. Isso leva à degradação do desempenho em dispositivos fracos (iPhone SE, iPad mini) e aumenta o tempo de inicialização da tela. LazyVStack carregará apenas as 10–12 linhas visíveis, criando o resto conforme você rola.
Um teste prático (usando Xcode Instruments, perfil Allocations) mostra: em um iPhone 12 mini, uma lista de 5000 elementos com LazyVStack consome 3–5 MB de memória, enquanto VStack com o mesmo conteúdo consome 150–250 MB — 50 vezes mais. Enquanto isso, o tempo de renderização inicial para LazyVStack é ~50 ms contra ~800 ms para VStack no mesmo dispositivo.
Escolha VStack para listas estáticas ou curtas (até 10–15 elementos), e LazyVStack para qualquer lista dinâmica ou potencialmente longa. A Apple recomenda usar LazyVStack por padrão se você não tiver certeza sobre o tamanho máximo da lista.
VStack continua sendo a melhor escolha para interfaces estáticas: tela de perfil, formulário de login, cartão de produto — onde o número de elementos é conhecido e não excede 10–15. VStack funciona mais rápido na renderização inicial para tais quantidades porque não desperdiça recursos no rastreamento de geometria e carregamento preguiçoso. Além disso, VStack funciona corretamente fora da ScrollView (por exemplo, dentro de ZStack ou Group), enquanto LazyVStack sem ScrollView perde seu propósito.
Os stacks preguiçosos são ideais para cenários com número grande ou imprevisível de elementos: feeds de redes sociais, catálogos de produtos, listas de chat, bibliotecas de arquivos de mídia, logs de eventos, painéis de administração com milhares de registros.
Casos de uso específicos: lista de mensagens em um mensageiro (dezenas de milhares de mensagens), carrossel de imagens em um aplicativo de galeria, feed de notícias com carregamento infinito, lista de pedidos em uma loja online. LazyHStack é especialmente útil para carrosséis horizontais — por exemplo, Stories do Instagram ou banners promocionais.
Contraindicações: interfaces com animações de aparecimento de elementos (stacks lazy não suportam transições entre estados de exclusão de elementos sem lógica adicional), casos onde todos os elementos devem ser visíveis simultaneamente (uma lista curta de caixas de seleção), e quando você precisa de controle preciso sobre a reutilização de células (neste caso, List ou Table podem ser preferíveis).
Um exemplo básico exibe 1000 elementos com consumo mínimo de memória. Elementos-chave: ScrollView como contêiner de rolagem, LazyVStack para carregamento preguiçoso, ForEach com um identificador para iteração de dados.
import SwiftUI
struct LazyListExample: View {
let items = Array(0..<1000)
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items, id: \.self) { index in
Text("Item #\(index)")
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
}
}
O código cria uma ScrollView contendo um LazyVStack com espaçamento de 8pt entre elementos. ForEach itera sobre o array items e cria um Text para cada índice. Graças ao carregamento preguiçoso, de 1000 elementos apenas os 10–12 visíveis estão na memória de cada vez.
Este exemplo demonstra o agrupamento de elementos por seções com cabeçalhos fixados, similar aos contatos do iOS. Section define o cabeçalho e o conteúdo, pinnedViews: .sectionHeaders fixa o cabeçalho no topo da tela ao rolar.
import SwiftUI
struct SectionedList: View {
let cities = ["Moscou", "Londres", "Tóquio", "Nova York", "Paris"]
let countries = ["Rússia", "Reino Unido", "Japão", "EUA", "França"]
var body: some View {
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
Section(header: Text("Cidades").font(.title).bold()) {
ForEach(cities, id: \.self) { city in
Text(city).padding(8)
}
}
Section(header: Text("Países").font(.title).bold()) {
ForEach(countries, id: \.self) { country in
Text(country).padding(8)
}
}
}
}
}
}
Os cabeçalhos fixados (.sectionHeaders) se comportam como section headers do UITableView: ao rolar uma seção, o cabeçalho "gruda" na borda superior da tela até que toda a seção desapareça, após o que é substituído pelo cabeçalho da próxima seção. pinnedViews podem ser combinados: .sectionHeaders e .sectionFooters simultaneamente.
LazyHStack é usado para rolagem horizontal — carrosséis de imagens, listas horizontais de categorias. O parâmetro alignment: .top alinha os elementos à borda superior.
import SwiftUI
struct HorizontalCarousel: View {
let colors: [Color] = [.red, .blue, .green, .orange, .purple, .pink]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16, alignment: .top) {
ForEach(0..<100, id: \.self) { index in
RoundedRectangle(cornerRadius: 12)
.fill(colors[index % colors.count])
.frame(width: 150, height: 200)
.overlay(Text("\(index + 1)").foregroundColor(.white).bold())
}
}
.padding(.horizontal)
}
.frame(height: 220)
}
}
O código cria uma ScrollView horizontal com LazyHStack. De 100 retângulos, apenas 2–3 são exibidos simultaneamente (dependendo da largura da tela e do tamanho dos elementos). Ao rolar para a esquerda, novos elementos são carregados preguiçosamente. A altura do contêiner é fixa (220pt) para evitar altura infinita na rolagem horizontal.
PinnedScrollableViews é uma opção de configuração para LazyVStack e LazyHStack que controla a fixação de cabeçalhos e rodapés de seção ao rolar. Dois valores são suportados: sectionHeaders (cabeçalhos grudam no início do contêiner) e sectionFooters (rodapés grudam no final).
O mecanismo de visualizações fixadas funciona apenas dentro de um contêiner Section aninhado no LazyVStack. Cada Section tem um header e/ou footer que automaticamente obtêm o comportamento de aderência. O SwiftUI rastreia a posição de cada seção em relação aos limites da ScrollView e alterna a visibilidade do elemento fixado ao fazer a transição entre seções.
Importante: pinnedViews aumenta a complexidade do cálculo de layout, pois o SwiftUI deve recalcular constantemente qual cabeçalho está atualmente fixado. Use pinnedViews apenas quando a funcionalidade for realmente necessária — para listas simples sem seções, é melhor omitir este parâmetro. A Apple em sua documentação (Human Interface Guidelines, 2024) recomenda o uso de cabeçalhos fixados para índices alfabéticos e agrupamento por datas.
O uso correto de identificadores é o fator de desempenho mais importante para LazyVStack. Cada elemento no ForEach deve ter um id único e estável. Usar \.self com primitivos (Int, String) é aceitável, mas para modelos de dados sempre implemente o protocolo Identifiable. IDs instáveis (por exemplo, UUID gerado a cada vez) fazem o SwiftUI recriar todas as visualizações a cada atualização.
Evite cálculos pesados dentro do body de cada elemento do stack. Se um elemento contiver layout complexo ou processamento de dados — extraia a lógica para uma estrutura de visualização separada com seu próprio carregamento preguiçoso. Use EquatableView para evitar redesenhos desnecessários quando os dados do elemento não mudaram.
Para imagens dentro de LazyVStack, sempre use carregamento assíncrono (AsyncImage) ou cache via Kingfisher/Nuke. Cada elemento não deve carregar uma imagem de forma síncrona ao aparecer na tela — isso causará travamentos na rolagem. De acordo com a WWDC Session 10031, o tamanho ideal do buffer de pré-carregamento é de 3–5 telas à frente e atrás da posição atual.
Meça o desempenho usando Xcode Instruments com o perfil SwiftUI. Preste atenção às métricas: avaliações do body, alocações e taxa de quadros (FPS). Valores alvo: FPS > 55 ao rolar, tempo de renderização por elemento < 1 ms.
Perguntas frequentes
List fornece capacidades integradas: edição por deslize (swipeActions), exclusão via .onDelete, reordenação via .onMove, estilo agrupado .insetGrouped. LazyVStack é uma ferramenta de nível mais baixo sem suporte integrado para gestos de edição. List usa LazyVStack internamente mas adiciona o estilo nativo de tabela do iOS. Se você precisa de um design de célula personalizado e não precisa de edição integrada — escolha LazyVStack. Se você precisa de swipeActions, .onDelete e trabalho com @FetchRequest — use List.
Os stacks preguiçosos usam pré-carregamento — o SwiftUI cria elementos com um pequeno buffer de antecipação (prefetch buffer) para garantir uma rolagem suave. O tamanho do buffer se ajusta automaticamente à velocidade de rolagem e ao desempenho do dispositivo. De acordo com dados de perfil da Apple, o buffer de pré-carregamento é geralmente de 1 a 3 telas na direção da rolagem. Se você vir muitos elementos invisíveis sendo criados, verifique se você tem identificadores gerados a cada vez ou cálculos pesados no inicializador da visualização.
Sim, mas com limitações. Aninhar LazyVStack dentro de VStack não faz sentido — o VStack externo criará todos os elementos do LazyVStack interno imediatamente, cancelando o carregamento preguiçoso. Aninhar VStack dentro de LazyVStack é aceitável e não quebra o mecanismo lazy. Aninhar LazyVStack dentro de outro LazyVStack é aceitável para seções aninhadas, mas fique de olho no desempenho: cada nível adiciona sobrecarga no rastreamento de geometria.
O SwiftUI não fornece divisores integrados para LazyVStack. Adicione-os manualmente: coloque Divider() após cada elemento no ForEach, ou use o modificador .overlay(Divider(), alignment: .bottom) em cada elemento. Para divisores personalizados, desenhe Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)).
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também