Chapter 05

导航与路由

掌握 NavigationStack、TabView、Sheet 等导航模式,设计清晰的 App 页面结构

5.1 导航模式概览

iOS 应用的导航结构主要由以下几种模式组成,它们往往组合使用:

导航方式视觉效果典型场景
NavigationStack从右侧推入新页面(Push)主从页面、详情页、设置层级
TabView底部标签栏切换App 主体结构(首页/搜索/我的)
sheet从底部滑入(模态)表单、快速操作、预览
fullScreenCover全屏覆盖(模态)登录页、相机、引导页
Alert弹窗确认操作、错误提示
ConfirmationDialog底部 Action Sheet多选操作

5.2 NavigationStack(iOS 16+)

NavigationStack 是 iOS 16 引入的新一代导航容器,取代了旧版的 NavigationView。它基于值驱动路由,比旧方式更强大、更可预测。

ℹ️
NavigationView vs NavigationStack

iOS 16 之前使用 NavigationView,存在多栏样式混乱、路由不可控等问题。iOS 16+ 推荐始终使用 NavigationStack,它支持深度链接、路由历史管理等高级特性。

基础用法:NavigationLink

struct BasicNavigationView: View {
    var body: some View {
        NavigationStack {
            List {
                // NavigationLink:点击后 push 到目标视图
                NavigationLink("第一页", destination: FirstDetailView())
                NavigationLink("第二页", destination: SecondDetailView())

                // 自定义链接外观
                NavigationLink(destination: ProfileView()) {
                    HStack {
                        Image(systemName: "person.circle")
                        Text("个人资料")
                    }
                }
            }
            .navigationTitle("主页")                          // 导航栏标题
            .navigationBarTitleDisplayMode(.large)            // 大标题模式
        }
    }
}

// 详情页:使用 .inline 小标题
struct FirstDetailView: View {
    var body: some View {
        Text("这是第一页的详情")
            .navigationTitle("详情")
            .navigationBarTitleDisplayMode(.inline)    // 小标题
    }
}

navigationTitle 与 navigationBarItems

struct ListPageView: View {
    @State private var items = ["苹果", "香蕉", "橙子"]
    @State private var showAdd = false

    var body: some View {
        List(items, id: \.self) { Text($0) }
            .navigationTitle("水果列表")
            // toolbar:在导航栏添加按钮(推荐方式)
            .toolbar {
                // 右上角按钮
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        showAdd = true
                    } label: {
                        Image(systemName: "plus")
                    }
                }

                // 左上角按钮
                ToolbarItem(placement: .topBarLeading) {
                    EditButton()    // 系统内置编辑按钮
                }

                // 底部工具栏(需要 .bottomBar placement)
                ToolbarItem(placement: .bottomBar) {
                    Text("\(items.count) 项").foregroundColor(.secondary)
                }
            }
    }
}

// ToolbarItem placement 位置选项:
// .topBarLeading      — 导航栏左侧
// .topBarTrailing     — 导航栏右侧
// .bottomBar          — 底部工具栏
// .principal          — 导航栏中间(通常放自定义标题)
// .confirmationAction — 确认操作(如"完成")
// .cancellationAction — 取消操作

5.3 类型安全路由:navigationDestination

navigationDestination(for:) 是 iOS 16 引入的类型安全路由机制。通过将路由信息建模为值类型,实现了真正的数据驱动导航

// 定义路由目的地类型(需要 Hashable)
struct Article: Hashable, Identifiable {
    let id: Int
    let title: String
    let content: String
}

struct Author: Hashable, Identifiable {
    let id: Int
    let name: String
}

// NavigationStack + navigationDestination
struct ArticleListView: View {
    let articles: [Article] = [
        Article(id: 1, title: "SwiftUI 入门", content: "..."),
        Article(id: 2, title: "状态管理", content: "..."),
    ]

    var body: some View {
        NavigationStack {
            List(articles) { article in
                // NavigationLink 传入值而非视图
                NavigationLink(value: article) {
                    Text(article.title)
                }
            }
            .navigationTitle("文章列表")
            // 在 NavigationStack 或任意祖先视图中注册目的地
            .navigationDestination(for: Article.self) { article in
                ArticleDetailView(article: article)
            }
            .navigationDestination(for: Author.self) { author in
                AuthorView(author: author)
            }
        }
    }
}

5.4 NavigationPath — 编程式路由控制

NavigationPath 是一个类型擦除的路由栈,允许你用代码控制导航历史,实现深度链接、直接跳转等高级功能。

struct RootNavigationView: View {
    // NavigationPath 管理路由历史栈
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {    // 绑定路由栈
            VStack(spacing: 20) {

                // 编程式 push:直接跳转到指定页面
                Button("直接打开文章 #5") {
                    path.append(Article(id: 5, title: "高级话题", content: "..."))
                }

                // 跳转多层
                Button("直接到三级页面") {
                    path.append(Category(id: 1, name: "技术"))   // 第二层
                    path.append(Article(id: 3, title: "...", content: "...")) // 第三层
                }

                // 返回根页面(清空路由栈)
                Button("回到根页面") {
                    path = NavigationPath()   // 重置为空
                }

                // 返回上一页(移除最后一个路由)
                Button("返回上一页") {
                    path.removeLast()
                }

                Text("当前路由深度:\(path.count)")
            }
            .navigationTitle("首页")
            .navigationDestination(for: Article.self) { article in
                ArticleDetailView(article: article)
            }
        }
    }
}

5.5 TabView — 底部标签栏

TabView 是构建 App 主框架的标准方式。通过 tabItem 修饰符为每个标签页设置图标和标题。

struct MainTabView: View {
    // 控制当前选中的标签(可省略,TabView 自动管理)
    @State private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {

            // 第一个标签:首页(包含独立的 NavigationStack)
            NavigationStack {
                HomeView()
            }
            .tabItem {
                Label("首页", systemImage: "house.fill")
            }
            .tag(0)   // 对应 selectedTab 的值

            // 第二个标签:搜索
            NavigationStack {
                SearchView()
            }
            .tabItem {
                Label("搜索", systemImage: "magnifyingglass")
            }
            .tag(1)

            // 第三个标签:带角标
            NotificationView()
            .tabItem {
                Label("通知", systemImage: "bell.fill")
            }
            .badge(3)      // 红色角标数字
            .tag(2)

            // 第四个标签:个人中心
            ProfileView()
            .tabItem {
                Label("我的", systemImage: "person.fill")
            }
            .tag(3)
        }
        .tint(.blue)   // 选中标签的颜色
    }
}

// 编程式切换标签
struct HomeView: View {
    @Binding var selectedTab: Int

    var body: some View {
        Button("去搜索页") {
            selectedTab = 1   // 切换到搜索标签
        }
    }
}

5.6 Sheet — 模态半屏视图

sheet 从屏幕底部滑入一个新视图,覆盖在当前页面上方。用于不需要完全离开当前上下文的操作,如添加记录、查看详情。

struct ArticleView: View {
    @State private var showAddSheet = false
    @State private var selectedArticle: Article? = nil

    var body: some View {
        VStack {
            // 方式1:isPresented 布尔控制
            Button("添加文章") { showAddSheet = true }
            .sheet(isPresented: $showAddSheet) {
                AddArticleView()                  // sheet 内容
            }

            // 方式2:item 可选值控制(有值时展示,nil 时关闭)
            List(articles) { article in
                Text(article.title)
                    .onTapGesture { selectedArticle = article }
            }
            .sheet(item: $selectedArticle) { article in
                ArticleDetailSheet(article: article)
            }
        }
    }
}

// Sheet 内部:用 dismiss 关闭
struct AddArticleView: View {
    @Environment(\.dismiss) var dismiss
    @State private var title = ""

    var body: some View {
        NavigationStack {
            Form {
                TextField("标题", text: $title)
            }
            .navigationTitle("新建文章")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("取消") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("保存") {
                        // 保存逻辑
                        dismiss()
                    }
                    .disabled(title.isEmpty)
                }
            }
        }
    }
}

// presentationDetents:控制 sheet 高度(iOS 16+)
struct QuickActionView: View {
    @State private var showSheet = false

    var body: some View {
        Button("操作面板") { showSheet = true }
        .sheet(isPresented: $showSheet) {
            ActionPanelView()
                .presentationDetents([.height(200), .medium, .large])
                // .height(200) — 固定 200pt 高
                // .medium     — 半屏(约 50%)
                // .large      — 全屏
                // .fraction(0.3) — 30% 屏幕高度
                .presentationDragIndicator(.visible)   // 显示顶部拖动条
        }
    }
}

5.7 FullScreenCover — 全屏覆盖

fullScreenCoversheet 类似,但会完全遮盖整个屏幕,通常用于登录、引导、相机等需要完全占据屏幕的场景。

struct AppRootView: View {
    @State private var isLoggedIn = false

    var body: some View {
        MainContentView()
            // 未登录时显示全屏登录页
            .fullScreenCover(isPresented: Binding(
                get: { !isLoggedIn },
                set: { _ in }
            )) {
                LoginView(isLoggedIn: $isLoggedIn)
            }
    }
}

struct LoginView: View {
    @Binding var isLoggedIn: Bool
    @Environment(\.dismiss) var dismiss

    var body: some View {
        VStack(spacing: 24) {
            Text("欢迎登录").font(.largeTitle).fontWeight(.bold)

            Button("一键登录") {
                isLoggedIn = true
                dismiss()
            }
            .buttonStyle(.borderedProminent)
            .controlSize(.large)
        }
    }
}

5.8 Alert 与 ConfirmationDialog

用于展示需要用户确认的操作或系统消息。Alert 是简单对话框,ConfirmationDialog(即 Action Sheet)适合多选操作。

struct DeleteExampleView: View {
    @State private var showDeleteAlert = false
    @State private var showActionSheet = false
    @State private var alertMessage = ""

    var body: some View {
        VStack(spacing: 20) {

            // ── Alert:简单确认弹窗 ──
            Button("删除账号", role: .destructive) {
                showDeleteAlert = true
            }
            .alert("确认删除", isPresented: $showDeleteAlert) {
                // 操作按钮(不写就只有默认的"OK")
                Button("删除", role: .destructive) {
                    // 执行删除
                    alertMessage = "已删除"
                }
                Button("取消", role: .cancel) { }
            } message: {
                Text("此操作不可撤销,账号中的所有数据将被永久删除。")
            }

            // Alert 绑定错误类型(item 方式)
            Button("触发错误") {
                alertMessage = "网络连接失败,请检查网络设置"
                showDeleteAlert = true
            }

            // ── ConfirmationDialog:Action Sheet(多个选项)──
            Button("分享") {
                showActionSheet = true
            }
            .confirmationDialog(
                "选择分享方式",
                isPresented: $showActionSheet,
                titleVisibility: .visible    // .visible / .hidden / .automatic
            ) {
                Button("复制链接") { }
                Button("发送给朋友") { }
                Button("保存到相册") { }
                Button("举报", role: .destructive) { }
                Button("取消", role: .cancel) { }
            } message: {
                Text("选择将此内容分享的方式")
            }
        }
    }
}

5.9 实战示例:带导航的主从页面

构建一个完整的新闻列表 App,综合运用 NavigationStack、TabView、Sheet 和 Alert。

// ─── 数据模型 ───
struct NewsItem: Identifiable, Hashable {
    let id = UUID()
    let title: String
    let summary: String
    let category: String
    let isBookmarked: Bool
}

// ─── 新闻详情页 ───
struct NewsDetailView: View {
    let news: NewsItem
    @State private var isBookmarked = false
    @State private var showShareSheet = false

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Text(news.category)
                    .font(.caption)
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .background(.blue.opacity(0.1))
                    .foregroundColor(.blue)
                    .cornerRadius(4)

                Text(news.title)
                    .font(.title)
                    .fontWeight(.bold)

                Divider()

                Text(news.summary)
                    .font(.body)
                    .lineSpacing(6)
                    .foregroundColor(.secondary)
            }
            .padding()
        }
        .navigationTitle("详情")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItemGroup(placement: .topBarTrailing) {
                Button {
                    isBookmarked.toggle()
                } label: {
                    Image(systemName: isBookmarked ? "bookmark.fill" : "bookmark")
                }

                Button {
                    showShareSheet = true
                } label: {
                    Image(systemName: "square.and.arrow.up")
                }
            }
        }
        .onAppear { isBookmarked = news.isBookmarked }
        .sheet(isPresented: $showShareSheet) {
            ShareSheetView(title: news.title)
                .presentationDetents([.medium])
        }
    }
}

// ─── 新闻列表 ───
struct NewsListView: View {
    let newsList: [NewsItem] = [
        NewsItem(title: "Apple 发布 SwiftUI 5.0",
                  summary: "本次更新带来了全新的动画系统和更多的系统集成...",
                  category: "科技", isBookmarked: false),
        NewsItem(title: "WWDC 2025 亮点回顾",
                  summary: "今年 WWDC 带来了 iOS 19、visionOS 3 等重大发布...",
                  category: "活动", isBookmarked: true),
    ]
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(newsList) { news in
                NavigationLink(value: news) {
                    VStack(alignment: .leading, spacing: 4) {
                        HStack {
                            Text(news.category)
                                .font(.caption)
                                .foregroundColor(.blue)
                            Spacer()
                            if news.isBookmarked {
                                Image(systemName: "bookmark.fill")
                                    .font(.caption)
                                    .foregroundColor(.orange)
                            }
                        }
                        Text(news.title).font(.headline).lineLimit(2)
                        Text(news.summary).font(.caption)
                            .foregroundColor(.secondary).lineLimit(2)
                    }
                    .padding(.vertical, 4)
                }
            }
            .navigationTitle("今日新闻")
            .navigationDestination(for: NewsItem.self) { news in
                NewsDetailView(news: news)
            }
        }
    }
}

// ─── 完整的标签栏 App ───
struct NewsApp: View {
    var body: some View {
        TabView {
            NewsListView()
                .tabItem { Label("头条", systemImage: "newspaper") }

            Text("搜索页")
                .tabItem { Label("搜索", systemImage: "magnifyingglass") }

            Text("我的书签")
                .tabItem { Label("书签", systemImage: "bookmark") }
        }
    }
}
小结

本章学习了 SwiftUI 的完整导航体系:NavigationStack 处理层级导航,navigationDestination 实现类型安全路由,NavigationPath 支持编程式路由控制,TabView 构建 App 主框架,sheet/fullScreenCover 处理模态展示,Alert/ConfirmationDialog 实现用户确认操作。