Como pensar em SwiftUI: composição e árvore de Views
Em SwiftUI, a interface é construída compondo pequenas Views em estruturas maiores. Em vez de “desenhar” a tela manualmente, você descreve o que deve aparecer e como deve se comportar. O resultado é uma árvore de views: contêineres (como VStack) agrupam elementos (como Text e Button), e cada nó pode receber modificadores (como .padding() e .background()).
Uma regra prática: comece com uma estrutura de layout (V/H/ZStack), coloque dentro os componentes básicos (Text, Image, Button etc.) e, por último, aplique modificadores para espaçamento, tamanho e estilo.
Views básicas que você vai usar o tempo todo
- Text: exibe texto e suporta estilo via modificadores.
- Image: exibe imagens do sistema (SF Symbols) ou do Asset Catalog.
- Button: componente interativo com ação e conteúdo (label).
- Spacer: ocupa espaço flexível para empurrar elementos.
- Divider: linha divisória, útil para separar seções.
Text: estilo, alinhamento e limites
O Text é simples, mas o comportamento muda bastante conforme você limita largura/altura e define alinhamento. Alguns modificadores comuns:
.font(...)e.fontWeight(...).foregroundStyle(...)(ou.foregroundColorem versões antigas).multilineTextAlignment(...).lineLimit(...)e.minimumScaleFactor(...)
Text("Bem-vindo ao SwiftUI")
.font(.title)
.fontWeight(.semibold)
.foregroundStyle(.primary)
Text("Um texto maior que pode quebrar em várias linhas dependendo do espaço disponível.")
.font(.body)
.multilineTextAlignment(.leading)
.lineLimit(3)
.minimumScaleFactor(0.8)Passo a passo: um cabeçalho com subtítulo responsivo
Objetivo: criar um cabeçalho que se adapte a telas menores sem “estourar” o layout.
VStack(alignment: .leading, spacing: 8) {
Text("Minha Conta")
.font(.largeTitle)
.fontWeight(.bold)
Text("Gerencie seus dados e preferências com segurança.")
.font(.body)
.foregroundStyle(.secondary)
.lineLimit(2)
.minimumScaleFactor(0.85)
}
.padding()Image: SF Symbols, redimensionamento e recorte
Imagens em SwiftUI podem ser de duas fontes comuns: SF Symbols (Image(systemName:)) e assets do projeto (Image("nome")). Para controlar tamanho e proporção, use .resizable() com .scaledToFit() ou .scaledToFill().
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Image(systemName: "bell.fill")
.font(.system(size: 28))
.foregroundStyle(.blue)
Image("profile")
.resizable()
.scaledToFill()
.frame(width: 72, height: 72)
.clipShape(Circle())Dica importante: .resizable() muda o comportamento da imagem para aceitar redimensionamento. Sem ele, .frame pode apenas “recortar” a imagem em vez de escalá-la.
Button: ação, label e área de toque
Um Button recebe uma ação (closure) e um conteúdo visual (label). Você pode usar um texto simples ou compor uma label com ícone + texto.
Button("Continuar") {
print("Toquei em Continuar")
}
Button {
print("Abrir configurações")
} label: {
HStack(spacing: 8) {
Image(systemName: "gearshape.fill")
Text("Configurações")
}
}Passo a passo: botão com estilo e área de toque consistente
Objetivo: criar um botão “cheio” com cantos arredondados e largura adaptável.
Button {
print("Comprar")
} label: {
Text("Comprar")
.font(.headline)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.background(.blue)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal)Note que .frame(maxWidth: .infinity) faz o conteúdo tentar ocupar toda a largura disponível dentro do contêiner, e o .padding(.horizontal) cria “margens” externas.
Spacer e Divider: controle de espaço e separação
Spacer() ocupa espaço flexível. Em um HStack, ele empurra elementos para as extremidades. Em um VStack, ele empurra para cima/baixo.
HStack {
Text("Título")
.font(.headline)
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.secondary)
}
.padding()Divider() desenha uma linha separadora. Ele respeita o layout do contêiner (por exemplo, dentro de um VStack ele é horizontal).
VStack(spacing: 12) {
Text("Seção A")
Divider()
Text("Seção B")
}
.padding()Contêineres: VStack, HStack e ZStack
VStack: empilhamento vertical
VStack organiza views de cima para baixo. Você controla alinhamento e espaçamento:
VStack(alignment: .leading, spacing: 10) {
Text("Nome")
Text("Email")
Text("Telefone")
}
.padding()HStack: empilhamento horizontal
HStack organiza views da esquerda para a direita. Combine com Spacer para distribuir espaço:
HStack(spacing: 12) {
Image(systemName: "person.circle.fill")
.font(.system(size: 36))
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 2) {
Text("Ana Souza").font(.headline)
Text("Plano Premium").font(.subheadline).foregroundStyle(.secondary)
}
Spacer()
Button("Editar") {
print("Editar")
}
}
.padding()ZStack: sobreposição
ZStack empilha views no eixo Z (uma em cima da outra). Útil para colocar texto sobre uma imagem, ou criar cards com fundo.
ZStack(alignment: .bottomLeading) {
RoundedRectangle(cornerRadius: 16)
.fill(.purple.gradient)
.frame(height: 140)
VStack(alignment: .leading, spacing: 6) {
Text("Destaque")
.font(.headline)
Text("Oferta válida hoje")
.font(.subheadline)
.foregroundStyle(.white.opacity(0.9))
}
.foregroundStyle(.white)
.padding()
}
.padding(.horizontal)Modificadores: encadeamento e ordem importa
Modificadores retornam uma nova view (uma “view modificada”). Por isso, a ordem pode mudar o resultado. Dois exemplos clássicos:
Exemplo 1: padding antes/depois do background
| Objetivo | Código | Resultado esperado |
|---|---|---|
| Fundo abraça o texto | Text("Olá") .background(.yellow) .padding() | O fundo fica justo no texto; o padding vira margem externa. |
| Fundo abraça texto + padding | Text("Olá") .padding() .background(.yellow) | O fundo inclui o padding (um “badge” maior). |
Exemplo 2: frame e background
// Fundo apenas no tamanho do texto
Text("A")
.background(.red)
.frame(width: 80, height: 80)
// Fundo no tamanho do frame
Text("A")
.frame(width: 80, height: 80)
.background(.red)No segundo caso, o .background é aplicado depois que a view já tem 80x80, então o fundo ocupa esse tamanho.
Layout no SwiftUI: como o tamanho é calculado
Um modelo mental útil para entender layout:
- O pai propõe um tamanho (constraints/available space).
- O filho escolhe um tamanho dentro do que foi proposto (com base no conteúdo e modificadores).
- O pai posiciona o filho (alinhamento e stacks).
Alguns comportamentos importantes:
- Text e Image (sem resizable) tendem a usar seu “tamanho ideal” (intrínseco).
- Spacer expande para ocupar o espaço restante.
- .frame pode impor tamanho fixo (
width/height) ou limites flexíveis (maxWidth/maxHeight). - alignment em
framecontrola onde o conteúdo fica dentro do frame.
Frame e alignment na prática
Text("Alinhado à esquerda")
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(.gray.opacity(0.2))Aqui o texto fica à esquerda dentro de uma área que ocupa toda a largura disponível.
background e overlay: camadas atrás e na frente
.background desenha atrás da view. .overlay desenha por cima. Ambos respeitam o tamanho atual da view (por isso a ordem com .padding e .frame é relevante).
Badge com overlay
Text("PRO")
.font(.caption)
.fontWeight(.bold)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(.black)
.foregroundStyle(.white)
.clipShape(Capsule())
.overlay(
Capsule().stroke(.white.opacity(0.6), lineWidth: 1)
)Preview: iterar rápido e comparar tamanhos de iPhone
O Preview permite testar layout sem rodar no simulador a cada mudança. Você pode criar variações para comparar tamanhos e modo claro/escuro.
#Preview("iPhone SE") {
MinhaTela()
}
#Preview("iPhone 15 Pro") {
MinhaTela()
}
#Preview("Dark") {
MinhaTela()
.preferredColorScheme(.dark)
}Para comparar lado a lado, você também pode criar uma view “container” só para preview, empilhando versões em um VStack e separando com Divider().
Exercício 1: Card de perfil simples (VStack + HStack + Spacer)
Objetivo: montar um card com avatar, nome, status e um botão, responsivo em largura.
Passo a passo
- Crie um contêiner com
VStackpara organizar título e conteúdo. - Use
HStackpara avatar + textos + botão. - Use
Spacer()para empurrar o botão para a direita. - Aplique
padding,backgroundeoverlaypara aparência de card.
struct PerfilCardView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Perfil")
.font(.headline)
HStack(spacing: 12) {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 44))
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 2) {
Text("Ana Souza")
.font(.headline)
Text("Online")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Button("Editar") {
print("Editar")
}
.font(.subheadline)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.white)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.gray.opacity(0.25), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding()
.background(.gray.opacity(0.08))
}
}Variações para testar no Preview
- Troque o nome por um texto bem longo e observe se quebra ou empurra o botão.
- Adicione
.lineLimit(1)no nome e.truncationMode(.tail)para evitar quebra. - Teste em telas menores e maiores usando múltiplos previews.
#Preview("SE") { PerfilCardView() }
#Preview("Pro Max") { PerfilCardView() }Exercício 2: Tela de “Login” responsiva (ZStack + overlay + frame)
Objetivo: criar uma tela com fundo em gradiente, um card central e botões, mantendo boa leitura em diferentes tamanhos.
Passo a passo
- Use
ZStackpara fundo + conteúdo. - Crie um card com
VStacke limite a largura comframe(maxWidth:). - Use
paddingebackgroundpara separar o card do fundo.
struct LoginSimplesView: View {
var body: some View {
ZStack {
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
VStack(spacing: 16) {
Text("Entrar")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundStyle(.white)
VStack(alignment: .leading, spacing: 12) {
Text("Acesse sua conta")
.font(.headline)
Divider()
Button {
print("Entrar com Apple")
} label: {
HStack {
Image(systemName: "applelogo")
Text("Continuar com Apple")
.fontWeight(.semibold)
Spacer()
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.background(.black)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
Button {
print("Entrar com Email")
} label: {
HStack {
Image(systemName: "envelope.fill")
Text("Continuar com Email")
.fontWeight(.semibold)
Spacer()
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.background(.white)
.foregroundStyle(.black)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(.black.opacity(0.08), lineWidth: 1)
)
}
.padding(16)
.frame(maxWidth: 420)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.horizontal)
}
.padding(.vertical, 24)
}
}
}Testes de responsividade no Preview
#Preview("SE") {
LoginSimplesView()
}
#Preview("15 Pro") {
LoginSimplesView()
}
#Preview("Dark") {
LoginSimplesView()
.preferredColorScheme(.dark)
}Experimente reduzir o espaço vertical (por exemplo, simulando teclado) adicionando mais conteúdo acima e observe como o VStack se comporta. Ajuste spacing, padding e limites de lineLimit para manter a tela legível.
Exercício 3: Comparando modificadores (ordem e efeito visual)
Objetivo: criar uma “tela laboratório” para ver rapidamente como a ordem dos modificadores muda o layout.
struct ModificadoresLabView: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Group {
Text("A")
.padding()
.background(.yellow)
Text("B")
.background(.yellow)
.padding()
}
Divider()
Group {
Text("C")
.frame(width: 80, height: 80)
.background(.red)
Text("D")
.background(.red)
.frame(width: 80, height: 80)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}Use o Preview para observar: onde o fundo aparece? Qual elemento “cresce”? O que vira margem externa vs. interna? Esse tipo de laboratório acelera muito a compreensão de layout em SwiftUI.