O que é navegação no SwiftUI moderno
No SwiftUI atual, a navegação recomendada é baseada em NavigationStack, que mantém uma “pilha” de telas (stack). Cada vez que você navega para uma nova tela, um novo destino é empilhado; ao voltar, ele é desempilhado.
Existem duas formas comuns de navegar:
- Navegação declarativa com
NavigationLink: você descreve um link que leva a um destino. - Navegação por rotas com
path: você controla a pilha por meio de um array de rotas (ótimo para fluxos mais complexos e para “deep links”).
O par mais importante é: NavigationStack + navigationDestination. O navigationDestination registra como construir telas para um tipo de rota (por exemplo, um enum).
Projeto prático: lista → detalhe → edição (com atualização ao voltar)
Você vai implementar um fluxo completo:
- Lista de itens (seleção em lista e navegação)
- Detalhe com título dinâmico e botões na barra
- Edição que altera dados e, ao voltar, a lista/detalhe refletem a atualização
1) Modelo e dados de exemplo
Crie um modelo simples. Usaremos Identifiable para facilitar listas e Hashable para permitir uso em rotas.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
import SwiftUI
struct TaskItem: Identifiable, Hashable {
let id: UUID
var title: String
var notes: String
var isDone: Bool
init(id: UUID = UUID(), title: String, notes: String = "", isDone: Bool = false) {
self.id = id
self.title = title
self.notes = notes
self.isDone = isDone
}
}
extension Array where Element == TaskItem {
static var sample: [TaskItem] {
[
TaskItem(title: "Comprar frutas", notes: "Maçã, banana, uva"),
TaskItem(title: "Estudar SwiftUI", notes: "NavigationStack e rotas"),
TaskItem(title: "Caminhada", notes: "30 minutos", isDone: true)
]
}
}2) Estruturando rotas com enum (abordagem simples)
Uma forma prática de estruturar navegação é criar um enum de rotas. Cada caso representa uma tela e carrega os dados mínimos para reconstruí-la.
Aqui, o detalhe e a edição precisam saber qual item está sendo exibido/editado. Para manter a rota estável, é comum passar apenas o id.
enum Route: Hashable {
case detail(id: UUID)
case edit(id: UUID)
}3) Tela de Lista com NavigationStack e path
Vamos controlar a navegação por um path (array de Route). Assim você consegue empurrar telas programaticamente (por exemplo, ao tocar em um botão) e também manter o fluxo consistente.
Repare em dois pontos:
NavigationLink(value:)empilha uma rota nopathautomaticamente..navigationDestination(for:)define como construir a UI para cada rota.
struct TasksListView: View {
@State private var tasks: [TaskItem] = .sample
@State private var path: [Route] = []
var body: some View {
NavigationStack(path: $path) {
List {
Section("Tarefas") {
ForEach(tasks) { task in
NavigationLink(value: Route.detail(id: task.id)) {
HStack {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isDone ? .green : .secondary)
VStack(alignment: .leading) {
Text(task.title)
.font(.headline)
if !task.notes.isEmpty {
Text(task.notes)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
}
}
}
.navigationTitle("Minhas Tarefas")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
let new = TaskItem(title: "Nova tarefa")
tasks.insert(new, at: 0)
path.append(.detail(id: new.id))
} label: {
Image(systemName: "plus")
}
}
}
.navigationDestination(for: Route.self) { route in
switch route {
case .detail(let id):
TaskDetailView(taskID: id, tasks: $tasks, path: $path)
case .edit(let id):
TaskEditView(taskID: id, tasks: $tasks)
}
}
}
}
}4) Lidando com seleção em listas
No exemplo acima, a “seleção” acontece quando o usuário toca em uma linha, e isso empilha a rota do detalhe. Em apps reais, você pode precisar de uma seleção explícita (por exemplo, para destacar a célula selecionada ou habilitar ações).
Uma abordagem simples é manter um @State com o UUID selecionado e atualizá-lo no toque. Mesmo usando NavigationLink, você pode registrar a seleção para uso em lógica de UI.
// Exemplo conceitual (não obrigatório no fluxo principal)
@State private var selectedID: UUID?
// Dentro do ForEach:
Button {
selectedID = task.id
path.append(.detail(id: task.id))
} label: {
// sua célula
}
.buttonStyle(.plain)Use isso quando você precisa de comportamento adicional além de “navegar ao tocar”. Para o fluxo padrão, NavigationLink costuma ser suficiente.
5) Tela de Detalhe com título dinâmico e botões na barra
A tela de detalhe precisa:
- Encontrar o item pelo
id - Exibir informações
- Ter um botão “Editar” na barra que navega para a tela de edição
- Permitir ações que alterem o item (ex.: marcar como concluído) e refletir isso ao voltar
Para atualizar dados, vamos passar $tasks (binding do array) e editar o item por índice. Isso garante que alterações feitas no detalhe ou na edição reflitam na lista.
struct TaskDetailView: View {
let taskID: UUID
@Binding var tasks: [TaskItem]
@Binding var path: [Route]
private var index: Int? {
tasks.firstIndex { $0.id == taskID }
}
var body: some View {
Group {
if let index {
let task = tasks[index]
Form {
Section("Status") {
Toggle("Concluída", isOn: $tasks[index].isDone)
}
Section("Detalhes") {
LabeledContent("Título") {
Text(task.title)
}
if !task.notes.isEmpty {
LabeledContent("Notas") {
Text(task.notes)
}
}
}
}
.navigationTitle(task.title) // título dinâmico
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Editar") {
path.append(.edit(id: taskID))
}
}
}
} else {
ContentUnavailableView("Tarefa não encontrada", systemImage: "exclamationmark.triangle")
.navigationTitle("Detalhe")
}
}
}
}6) Tela de Edição: passando dados e salvando alterações
Na edição, você geralmente não quer alterar o modelo a cada tecla digitada (depende do caso). Um padrão comum é:
- Copiar os valores para um estado local (
draft) - Ao tocar em “Salvar”, aplicar as mudanças no array original
Isso evita alterações parciais caso o usuário volte sem salvar.
struct TaskEditView: View {
let taskID: UUID
@Binding var tasks: [TaskItem]
@Environment(\.dismiss) private var dismiss
@State private var title: String = ""
@State private var notes: String = ""
@State private var isDone: Bool = false
private var index: Int? {
tasks.firstIndex { $0.id == taskID }
}
var body: some View {
Form {
Section("Campos") {
TextField("Título", text: $title)
TextField("Notas", text: $notes, axis: .vertical)
.lineLimit(3...6)
Toggle("Concluída", isOn: $isDone)
}
}
.navigationTitle("Editar")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancelar") {
dismiss()
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Salvar") {
save()
dismiss()
}
.fontWeight(.semibold)
.disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
.onAppear {
loadDraft()
}
}
private func loadDraft() {
guard let index else { return }
title = tasks[index].title
notes = tasks[index].notes
isDone = tasks[index].isDone
}
private func save() {
guard let index else { return }
tasks[index].title = title
tasks[index].notes = notes
tasks[index].isDone = isDone
}
}Como o retorno atualiza os dados automaticamente
O retorno (back) não precisa de código especial para “recarregar” a lista: como a lista, o detalhe e a edição estão ligados ao mesmo estado (@State tasks na raiz e @Binding nas telas), qualquer alteração em tasks dispara atualização de UI.
O fluxo fica assim:
- Lista mantém
@State tasks - Detalhe recebe
@Binding taskse altera diretamente um item (ex.: toggle de concluída) - Edição recebe
@Binding tasks, aplica mudanças no “Salvar” e fecha comdismiss() - Ao voltar, o detalhe e a lista já refletem o novo título/estado
NavigationLink direto vs. rotas: quando usar cada um
| Abordagem | Quando usar | Como fica |
|---|---|---|
NavigationLink(destination:) | Fluxos simples e locais, sem necessidade de controlar a pilha | Link aponta diretamente para uma View |
NavigationLink(value:) + navigationDestination | Quando você quer centralizar destinos e padronizar rotas | Link empilha um valor (rota) e o destino é resolvido por tipo |
NavigationStack(path:) | Quando precisa navegar programaticamente, resetar fluxo, deep link, múltiplas telas empilhadas | Você controla a pilha via array de rotas |
Passo a passo resumido para montar o fluxo no seu app
- Crie o modelo (
TaskItem) comide propriedades editáveis. - Defina um enum de rotas (
Route) com casos para cada tela e dados mínimos (ex.:id). - Na tela raiz, crie
NavigationStack(path:)e um@Stateparapath. - Use
NavigationLink(value:)na lista para empilhar.detail(id:). - Registre
.navigationDestination(for: Route.self)e faça oswitchpara construir as telas. - No detalhe, use título dinâmico (
.navigationTitle(task.title)) e um botão “Editar” na toolbar que fazpath.append(.edit(id:)). - Na edição, carregue um draft no
.onAppear, valide e salve no array via@Binding, depois feche comdismiss().
Variações úteis: resetar a pilha e voltar para a raiz
Como você controla o path, pode voltar para a raiz limpando-o. Isso é útil após finalizar um fluxo.
// Em qualquer lugar que tenha acesso ao binding do path:
path.removeAll()Também é possível remover apenas a última tela (equivalente a “voltar” uma vez):
if !path.isEmpty {
path.removeLast()
}