SwiftUI: Construindo Interfaces Modernas para iOS
Guia completo para criar interfaces modernas com SwiftUI: sintaxe declarativa, componentes, animacoes e boas praticas para iOS 18.

SwiftUI transformou o desenvolvimento de interfaces nas plataformas Apple. Com sua sintaxe declarativa e integracao nativa, o framework permite construir aplicativos elegantes com menos linhas de codigo do que nunca. O iOS 18 traz melhorias expressivas em desempenho e novas funcionalidades.
O SwiftUI atingiu maturidade com o iOS 18, oferecendo renderizacao otimizada, melhor gerenciamento de memoria e integracao simplificada com UIKit. Tornou-se o padrao para novos aplicativos iOS.
Entendendo o Paradigma Declarativo
Antes de escrever codigo, vale compreender o que torna o SwiftUI diferente. Com o UIKit (o framework anterior), era necessario dizer ao iOS como construir a interface passo a passo: "crie um label, posicione aqui, mude a cor quando o usuario tocar". Esse e o paradigma imperativo.
O SwiftUI funciona de forma diferente: descreve-se o que deve ser exibido e o framework cuida do resto. E como a diferenca entre dar instrucoes de GPS curva a curva (imperativo) e simplesmente informar o destino (declarativo).
A Primeira View
No SwiftUI, cada elemento de interface e uma View. Uma View e uma struct que descreve o que deve aparecer na tela. A seguir, uma primeira tela com titulo, subtitulo e botao:
import SwiftUI
// Each screen is a struct that implements the View protocol
struct ContentView: View {
// The "body" property describes what the view displays
// "some View" means "a type of View, but Swift figures it out automatically"
var body: some View {
// VStack = "Vertical Stack": stacks elements vertically
// spacing: 20 = 20 points of space between each element
VStack(spacing: 20) {
// A simple text with style modifiers
Text("Welcome to SharpSkill")
.font(.largeTitle) // Large title font
.fontWeight(.bold) // Bold text
Text("Prepare for your iOS interviews")
.font(.subheadline) // Smaller font
.foregroundColor(.secondary) // Gray secondary color
// Button with an action (closure) and a label
Button("Get Started") {
print("Button tapped!")
}
.buttonStyle(.borderedProminent) // Filled blue button style
}
.padding() // Adds margins around the VStack
}
}A estrutura e direta: declaram-se os elementos desejados (textos, botao), empilham-se verticalmente (VStack) e aplicam-se estilos por meio de modificadores (.font(), .padding()).
Ponto-chave: No SwiftUI, nao se "criam" objetos de UI -- descreve-se a interface desejada. O SwiftUI cuida de criar, atualizar e destruir os elementos reais.
Modificadores: Transformando as Views
Modificadores sao metodos encadeados apos uma view para transforma-la. Funcionam como filtros do Instagram aplicados um apos o outro. A ordem importa, pois cada modificador cria uma nova view que envolve a anterior.
O exemplo a seguir ilustra por que a ordem e crucial:
// Example 1: padding THEN background
Text("SwiftUI")
.padding() // 1. Adds 16pt of space around the text
.background(.blue) // 2. Blue background covers text + padding
.foregroundColor(.white)
// Result: white text on blue rectangle with margins
// Example 2: background THEN padding (reversed order!)
Text("SwiftUI")
.background(.blue) // 1. Tight blue background around text only
.padding() // 2. Transparent padding around blue background
.foregroundColor(.white)
// Result: white text on small blue rectangle, surrounded by empty spaceA diferenca: no primeiro caso, o padding fica "dentro" do fundo. No segundo, fica "fora". Sutil, mas essencial para dominar layouts.
Organizando Interfaces com Stacks
O SwiftUI oferece tres containers principais para organizar views. Podem ser pensados como caixas que organizam seu conteudo de formas diferentes.
VStack, HStack e ZStack
- VStack (Vertical Stack): empilha elementos de cima para baixo
- HStack (Horizontal Stack): alinha elementos da esquerda para a direita
- ZStack (Z-axis Stack): sobrepoe elementos um sobre o outro
O proximo exemplo constroi um cartao de perfil de usuario combinando os tres tipos de stack. O objetivo: exibir uma foto com selo de verificacao, seguida do nome e funcao do usuario.
struct ProfileCard: View {
var body: some View {
// Main HStack: photo on left, info on right
HStack(spacing: 16) {
// ZStack to overlay the badge on the photo
ZStack(alignment: .bottomTrailing) {
// Profile image (circle)
Image("avatar")
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
// Green "verified" badge at bottom right
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.background(Circle().fill(.white)) // White circle behind
}
// VStack to stack name and role vertically
VStack(alignment: .leading, spacing: 4) {
Text("Marie Dupont")
.font(.headline)
Text("iOS Developer")
.font(.subheadline)
.foregroundColor(.secondary)
}
// Spacer pushes everything to the left
Spacer()
// Chevron on right indicates it's tappable
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
}O truque esta no Spacer(): e um elemento invisivel que ocupa todo o espaco disponivel. Sem ele, os elementos ficariam centralizados. Com ele, sao empurrados para a esquerda e o chevron permanece fixo a direita.
Usar command + click em qualquer view no Xcode abre o inspetor visual. E possivel adicionar modificadores sem digitar codigo.
Gerenciamento de Estado com @State e @Binding
O gerenciamento de estado e o coracao do SwiftUI. Estado e qualquer dado que pode mudar e que deve atualizar a interface. Quando o estado muda, o SwiftUI recalcula automaticamente as views afetadas.
@State: O Estado Local de uma View
@State e um property wrapper que diz ao SwiftUI: "observe esta variavel e atualize a view quando ela mudar". E ideal para o estado local de uma unica view.
O contador interativo a seguir permite entender o mecanismo:
struct CounterView: View {
// @State creates a "source of truth" for this view
// private because state shouldn't be modified from outside
@State private var count = 0
var body: some View {
VStack(spacing: 30) {
// This Text updates automatically when count changes
Text("\(count)")
.font(.system(size: 72, weight: .bold))
HStack(spacing: 40) {
// Decrement button
Button(action: {
count -= 1 // Modifies state → view refreshes
}) {
Image(systemName: "minus.circle.fill")
.font(.largeTitle)
}
// Increment button
Button(action: {
count += 1
}) {
Image(systemName: "plus.circle.fill")
.font(.largeTitle)
}
}
}
}
}Ao tocar em um botao, count muda. O SwiftUI detecta essa mudanca e executa novamente body para atualizar a tela. Nao e preciso atualizar manualmente o label -- tudo e automatico.
@Binding: Compartilhando Estado entre Views
As vezes, uma view filha precisa modificar o estado da view pai. E ai que entra o @Binding: ele cria uma conexao bidirecional com um @State existente.
Um exemplo concreto: um campo de nome de usuario com validacao em tempo real.
// Parent view: owns the state
struct SignupForm: View {
@State private var username = "" // Source of truth
@State private var isValid = false // Validation state
var body: some View {
VStack(spacing: 20) {
// We pass BINDINGS (with $) to the child view
UsernameField(username: $username, isValid: $isValid)
Button("Create Account") {
// Submit the form
}
.disabled(!isValid) // Disabled if invalid
.buttonStyle(.borderedProminent)
}
.padding()
}
}A view filha recebe bindings e pode modifica-los:
// Child view: receives and modifies state via @Binding
struct UsernameField: View {
@Binding var username: String // Connection to parent's @State
@Binding var isValid: Bool
var body: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Username", text: $username)
.textFieldStyle(.roundedBorder)
.onChange(of: username) { oldValue, newValue in
// Validation: at least 3 characters
isValid = newValue.count >= 3
}
// Visual feedback
HStack {
Image(systemName: isValid ? "checkmark.circle" : "xmark.circle")
Text("Minimum 3 characters")
}
.font(.caption)
.foregroundColor(isValid ? .green : .red)
}
}
}Quando o usuario digita no TextField, username e modificado pelo binding. A view pai enxerga essa alteracao e pode utiliza-la. Comunicacao bidirecional limpa.
Utilizar @State somente para estado simples e local. Para dados compartilhados entre varias telas ou logica complexa, o recomendado e usar @Observable (iOS 17+) ou padroes MVVM.
Pronto para mandar bem nas entrevistas de iOS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Exibindo Listas Dinamicas
Listas estao presentes em praticamente todo aplicativo movel. O SwiftUI disponibiliza List para exibir colecoes de dados com estilo nativo do iOS (separadores, acoes de swipe, etc.).
Criando uma Lista Simples
Para exibir uma lista sao necessarias duas coisas: dados e uma forma de identifica-los. O protocolo Identifiable permite ao SwiftUI saber qual view corresponde a qual dado.
Primeiro, a definicao do modelo de dados:
// Identifiable lets SwiftUI track each element
struct Interview: Identifiable {
let id = UUID() // Auto-generated unique identifier
let technology: String
let difficulty: String
let questionCount: Int
}Agora a lista. A ideia e iterar sobre os dados com ForEach e criar uma linha para cada elemento:
struct InterviewListView: View {
// Data to display (in reality, this would come from an API)
@State private var interviews = [
Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25),
Interview(technology: "Android", difficulty: "Advanced", questionCount: 30),
Interview(technology: "React", difficulty: "Beginner", questionCount: 20)
]
var body: some View {
// NavigationStack enables the navigation bar
NavigationStack {
List {
// ForEach iterates over each interview
// Thanks to Identifiable, no need to specify id:
ForEach(interviews) { interview in
InterviewRow(interview: interview)
}
}
.navigationTitle("My Interviews")
}
}
}E a view para cada linha, extraida em seu proprio componente para clareza:
struct InterviewRow: View {
let interview: Interview
var body: some View {
HStack {
// Left column: title and subtitle
VStack(alignment: .leading, spacing: 4) {
Text(interview.technology)
.font(.headline)
Text(interview.difficulty)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
// Badge with question count
Text("\(interview.questionCount) Q")
.font(.caption)
.fontWeight(.medium)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
.padding(.vertical, 4)
}
}Extrair InterviewRow em uma struct propria torna o codigo mais legivel e reutilizavel. Essa e uma boa pratica consolidada no SwiftUI.
Acoes: Excluir e Reordenar
As listas do iOS suportam nativamente exclusao (deslizar para a esquerda) e reordenamento (arrastar e soltar). O SwiftUI simplifica isso com .onDelete e .onMove:
struct InterviewListView: View {
@State private var interviews = [/* ... data ... */]
var body: some View {
NavigationStack {
List {
ForEach(interviews) { interview in
InterviewRow(interview: interview)
}
// Swipe to delete
.onDelete(perform: deleteInterview)
// Drag and drop to reorder
.onMove(perform: moveInterview)
}
.navigationTitle("My Interviews")
.toolbar {
// "Edit" button that activates edit mode
EditButton()
}
}
}
// Deletes elements at specified indices
private func deleteInterview(at offsets: IndexSet) {
interviews.remove(atOffsets: offsets)
}
// Moves elements from one position to another
private func moveInterview(from source: IndexSet, to destination: Int) {
interviews.move(fromOffsets: source, toOffset: destination)
}
}Com apenas 4 linhas extras de codigo (.onDelete, .onMove, EditButton e as duas funcoes), obtem-se uma lista totalmente interativa. Essa e a eficiencia do SwiftUI.
Animando as Interfaces
O SwiftUI se destaca em animacoes. Diferentemente do UIKit, onde animacoes exigiam bastante codigo, aqui tudo e declarativo: descreve-se o estado final e o SwiftUI anima a transicao.
Animacoes Implicitas com withAnimation
A forma mais simples de animar e envolver uma mudanca de estado em withAnimation. O SwiftUI detecta o que muda e anima automaticamente as propriedades visuais afetadas.
struct AnimatedCard: View {
@State private var isExpanded = false
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
// Dimensions change based on state
.frame(
width: isExpanded ? 300 : 150,
height: isExpanded ? 200 : 100
)
Button(isExpanded ? "Collapse" : "Expand") {
// withAnimation animates ALL visual changes
// resulting from this state change
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
isExpanded.toggle()
}
}
.padding(.top)
}
}
}Ao tocar o botao, isExpanded alterna. Gracas ao withAnimation, a mudanca de tamanho do retangulo e animada com efeito de mola. Nao e necessario especificar o que animar -- o SwiftUI resolve sozinho.
Transicoes: Animando o Surgimento e Desaparecimento
Transicoes definem como uma view aparece ou desaparece. Por padrao e um fade (opacity), mas e possivel personalizar:
struct TransitionDemo: View {
@State private var showDetails = false
var body: some View {
VStack(spacing: 20) {
Button("Show Details") {
withAnimation(.easeInOut(duration: 0.3)) {
showDetails.toggle()
}
}
// This view appears/disappears with a transition
if showDetails {
DetailCard()
// Asymmetric transition: different for entry and exit
.transition(
.asymmetric(
insertion: .scale.combined(with: .opacity), // Entry: zoom + fade
removal: .slide // Exit: slide
)
)
}
}
}
}
struct DetailCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Interview Details")
.font(.headline)
Text("25 questions • 45 minutes")
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(16)
}
}Nesse exemplo, o cartao aparece com zoom a partir do centro (scale + opacity) e desaparece deslizando para o lado (slide). Essas microanimacoes fazem a interface parecer viva e profissional.
O SwiftUI otimiza animacoes automaticamente. Recomenda-se preferir .spring() para uma sensacao natural e evitar animacoes longas demais (> 0.5s) que frustram os usuarios.
Carregando Dados Assincronos
Em aplicativos reais, os dados frequentemente vem de uma API. O Swift Concurrency (async/await) se integra perfeitamente ao SwiftUI pelo modificador .task.
O Padrao Loading / Error / Success
Este e o padrao habitual para exibir dados de uma API. Tres estados sao gerenciados: carregando, erro e sucesso.
struct AsyncDataView: View {
@State private var questions: [Question] = []
@State private var isLoading = true
@State private var errorMessage: String?
var body: some View {
Group {
if isLoading {
// State: loading in progress
ProgressView("Loading...")
} else if let error = errorMessage {
// State: error
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundColor(.orange)
Text(error)
Button("Retry") {
Task { await loadQuestions() }
}
}
} else {
// State: success, display data
List(questions) { question in
Text(question.title)
}
}
}
// .task runs automatically when the view appears
.task {
await loadQuestions()
}
// Pull-to-refresh
.refreshable {
await loadQuestions()
}
}
private func loadQuestions() async {
isLoading = true
errorMessage = nil
do {
// Async API call
questions = try await QuestionService.shared.fetchQuestions()
} catch {
errorMessage = "Unable to load questions"
}
isLoading = false
}
}O modificador .task e essencial: ele inicia uma tarefa assincrona quando a view aparece e a cancela automaticamente quando desaparece. Sem possibilidade de vazamentos de memoria.
Conclusao
O SwiftUI se consolidou como ferramenta fundamental para o desenvolvimento moderno de iOS. Com o iOS 18, o framework atingiu uma maturidade que o torna perfeitamente adequado para aplicativos em producao.
Pontos-Chave
- Paradigma declarativo: descrever o que se deseja, nao como construir
- @State e @Binding: gerenciar estado de forma reativa e propaga-lo entre views
- Stacks: combinar VStack, HStack e ZStack para layouts flexiveis
- List: exibir colecoes com codigo minimo e interacoes nativas
- Animacoes: usar
withAnimationpara transicoes suaves automaticas - Async/await: carregar dados com
.taske gerenciar estados de carregamento e erro
Checklist
- Compreender a diferenca entre imperativo (UIKit) e declarativo (SwiftUI)
- Dominar modificadores e sua ordem de aplicacao
- Saber quando usar @State vs @Binding vs @Observable
- Construir layouts com Stacks
- Implementar listas com acoes (excluir, mover)
- Animar mudancas de estado com withAnimation
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
O SwiftUI abre portas para a construcao de aplicativos elegantes em todo o ecossistema Apple. O melhor caminho para aprender e praticando: criar um projeto pessoal pequeno e experimentar cada conceito deste artigo e o metodo mais eficaz.
Tags
Compartilhar
Artigos relacionados

Performance SwiftUI: Otimização de LazyVStack e Listas Complexas
Técnicas de otimização para LazyVStack e listas SwiftUI. Reduzir o consumo de memória, melhorar a performance de scroll e evitar armadilhas comuns.

ViewModifiers customizados em SwiftUI: padrões reutilizáveis para Design Systems
Construa ViewModifiers customizados em SwiftUI para um design system consistente. Padrões, melhores práticas e exemplos práticos para estilizar views iOS de forma eficiente.

SwiftUI @Observable vs @State: Quando Usar Cada Um em 2026
Domine as diferenças entre @Observable e @State no SwiftUI para escolher a ferramenta certa de gerenciamento de estado em aplicações iOS.