Navegação em apps iOS com SwiftUI: NavigationStack, rotas e telas

Capítulo 5

Tempo estimado de leitura: 7 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 no path automaticamente.
  • .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 tasks e altera diretamente um item (ex.: toggle de concluída)
  • Edição recebe @Binding tasks, aplica mudanças no “Salvar” e fecha com dismiss()
  • Ao voltar, o detalhe e a lista já refletem o novo título/estado

NavigationLink direto vs. rotas: quando usar cada um

AbordagemQuando usarComo fica
NavigationLink(destination:)Fluxos simples e locais, sem necessidade de controlar a pilhaLink aponta diretamente para uma View
NavigationLink(value:) + navigationDestinationQuando você quer centralizar destinos e padronizar rotasLink empilha um valor (rota) e o destino é resolvido por tipo
NavigationStack(path:)Quando precisa navegar programaticamente, resetar fluxo, deep link, múltiplas telas empilhadasVocê controla a pilha via array de rotas

Passo a passo resumido para montar o fluxo no seu app

  • Crie o modelo (TaskItem) com id e 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 @State para path.
  • Use NavigationLink(value:) na lista para empilhar .detail(id:).
  • Registre .navigationDestination(for: Route.self) e faça o switch para construir as telas.
  • No detalhe, use título dinâmico (.navigationTitle(task.title)) e um botão “Editar” na toolbar que faz path.append(.edit(id:)).
  • Na edição, carregue um draft no .onAppear, valide e salve no array via @Binding, depois feche com dismiss().

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()
}

Agora responda o exercício sobre o conteúdo:

Em um fluxo com NavigationStack(path:) e telas de lista, detalhe e edição, qual prática garante que as alterações feitas na edição apareçam automaticamente ao voltar para o detalhe e para a lista?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Com um @State na raiz e @Binding nas telas, todas leem e escrevem no mesmo estado. Assim, ao aplicar as mudanças no array (por exemplo, por índice) e voltar, a UI é atualizada automaticamente.

Próximo capitúlo

Listas no SwiftUI para iOS: List, ForEach, seções e desempenho básico

Arrow Right Icon
Capa do Ebook gratuito iOS para Iniciantes com SwiftUI: do zero ao primeiro app na App Store
36%

iOS para Iniciantes com SwiftUI: do zero ao primeiro app na App Store

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.