从零开始学iOS开发(第四十六篇):SwiftUI 导航与路由 —— 构建可扩展的导航架构

欢迎来到本系列教程的第四十六篇。在前四十五篇文章中,你已经学习了从Swift基础到数据可视化的全方位iOS开发技能。现在,你能够构建出功能完善、数据丰富的应用了。但是,当应用包含数十个页面时,如何管理复杂的导航逻辑?如何实现深度链接?如何处理状态恢复?

导航架构是大型应用的核心基础设施。SwiftUI提供了NavigationStack、NavigationPath等工具,结合自定义路由系统,可以构建可维护、可测试的导航架构。

在这一篇中,你将学到:

  1. NavigationStack基础

    • 声明式导航

    • NavigationPath状态管理

    • 导航样式与工具栏

  2. 枚举路由系统

    • 类型安全的导航

    • 参数传递

    • 嵌套导航

  3. 协调器模式

    • 分离导航逻辑

    • 依赖注入

    • 可测试性

  4. 深度链接

    • URL路由解析

    • 推送通知导航

    • 状态恢复

  5. 实战项目:构建完整的应用路由系统


一、NavigationStack基础

1.1 基础导航

swift

复制代码
import SwiftUI

// MARK: - 基础NavigationStack
struct BasicNavigationView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("用户详情", destination: UserDetailView(userId: 1))
                NavigationLink("设置页面", destination: SettingsView())
                NavigationLink("关于页面", destination: AboutView())
            }
            .navigationTitle("首页")
            .navigationBarTitleDisplayMode(.large)
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("编辑") {
                        // 编辑动作
                    }
                }
            }
        }
    }
}

struct UserDetailView: View {
    let userId: Int
    
    var body: some View {
        VStack {
            Text("用户ID: \(userId)")
            NavigationLink("查看订单", destination: OrderListView(userId: userId))
        }
        .navigationTitle("用户详情")
        .navigationBarTitleDisplayMode(.inline)
    }
}

swift

复制代码
// MARK: - NavigationPath导航
struct NavigationPathView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                Button("跳转到A -> B -> C") {
                    path.append("A")
                    path.append("B")
                    path.append("C")
                }
                
                Button("随机导航") {
                    let destinations = ["A", "B", "C", "D", "E"]
                    path.append(destinations.randomElement()!)
                }
            }
            .navigationTitle("导航路径")
            .navigationDestination(for: String.self) { value in
                DestinationView(value: value, path: $path)
            }
        }
    }
}

struct DestinationView: View {
    let value: String
    @Binding var path: NavigationPath
    
    var body: some View {
        VStack(spacing: 20) {
            Text("当前页面: \(value)")
                .font(.largeTitle)
            
            HStack {
                Button("返回上一页") {
                    path.removeLast()
                }
                .buttonStyle(.bordered)
                
                Button("返回首页") {
                    path.removeLast(path.count)
                }
                .buttonStyle(.bordered)
                
                Button("添加下一页") {
                    let next = String(UnicodeScalar(value.unicodeScalars.first!.value + 1)!)
                    if next <= "E" {
                        path.append(next)
                    }
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .navigationTitle("页面 \(value)")
    }
}

1.3 导航样式与工具栏

swift

复制代码
// MARK: - 自定义导航样式
struct CustomNavigationView: View {
    var body: some View {
        NavigationStack {
            List(0..<10) { i in
                NavigationLink("项目 \(i)", value: i)
            }
            .navigationTitle("自定义导航")
            .navigationBarTitleDisplayMode(.inline)
            .toolbarBackground(.visible, for: .navigationBar)
            .toolbarBackground(.blue.opacity(0.1), for: .navigationBar)
            .toolbarColorScheme(.dark, for: .navigationBar)
            .navigationDestination(for: Int.self) { item in
                DetailView(item: item)
            }
        }
    }
}

struct DetailView: View {
    let item: Int
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack(spacing: 20) {
            Text("详情页面 \(item)")
                .font(.largeTitle)
            
            Button("关闭") {
                dismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .navigationTitle("详情")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                Button("取消") {
                    dismiss()
                }
            }
            ToolbarItem(placement: .topBarTrailing) {
                Button("保存") {
                    // 保存动作
                }
            }
        }
    }
}

二、枚举路由系统

2.1 类型安全的导航

swift

复制代码
// MARK: - 路由枚举定义
enum AppRoute: Hashable {
    case home
    case userDetail(id: Int)
    case settings
    case about
    case orderList(userId: Int)
    case orderDetail(orderId: Int)
    case productDetail(productId: Int, fromCart: Bool)
}

// MARK: - 路由处理器
struct AppRouter: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            HomeView(path: $path)
                .navigationDestination(for: AppRoute.self) { route in
                    switch route {
                    case .home:
                        HomeView(path: $path)
                    case .userDetail(let id):
                        UserDetailRouteView(userId: id, path: $path)
                    case .settings:
                        SettingsRouteView(path: $path)
                    case .about:
                        AboutRouteView(path: $path)
                    case .orderList(let userId):
                        OrderListRouteView(userId: userId, path: $path)
                    case .orderDetail(let orderId):
                        OrderDetailRouteView(orderId: orderId, path: $path)
                    case .productDetail(let productId, let fromCart):
                        ProductDetailRouteView(productId: productId, fromCart: fromCart, path: $path)
                    }
                }
        }
    }
}

// MARK: - 首页视图
struct HomeView: View {
    @Binding var path: NavigationPath
    
    var body: some View {
        List {
            Button("用户详情") {
                path.append(AppRoute.userDetail(id: 123))
            }
            
            Button("设置") {
                path.append(AppRoute.settings)
            }
            
            Button("订单列表") {
                path.append(AppRoute.orderList(userId: 123))
            }
            
            Button("关于") {
                path.append(AppRoute.about)
            }
        }
        .navigationTitle("首页")
    }
}

struct UserDetailRouteView: View {
    let userId: Int
    @Binding var path: NavigationPath
    
    var body: some View {
        VStack(spacing: 20) {
            Text("用户ID: \(userId)")
            
            Button("查看订单") {
                path.append(AppRoute.orderList(userId: userId))
            }
            
            Button("查看商品") {
                path.append(AppRoute.productDetail(productId: 456, fromCart: false))
            }
        }
        .navigationTitle("用户详情")
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                Button("返回") {
                    path.removeLast()
                }
            }
        }
    }
}

// 其他路由视图类似...
struct SettingsRouteView: View {
    @Binding var path: NavigationPath
    var body: some View {
        Text("设置页面")
            .navigationTitle("设置")
    }
}

struct AboutRouteView: View {
    @Binding var path: NavigationPath
    var body: some View {
        Text("关于页面")
            .navigationTitle("关于")
    }
}

struct OrderListRouteView: View {
    let userId: Int
    @Binding var path: NavigationPath
    var body: some View {
        List {
            Button("订单 #1001") {
                path.append(AppRoute.orderDetail(orderId: 1001))
            }
            Button("订单 #1002") {
                path.append(AppRoute.orderDetail(orderId: 1002))
            }
        }
        .navigationTitle("订单列表")
    }
}

struct OrderDetailRouteView: View {
    let orderId: Int
    @Binding var path: NavigationPath
    var body: some View {
        Text("订单详情: \(orderId)")
            .navigationTitle("订单详情")
    }
}

struct ProductDetailRouteView: View {
    let productId: Int
    let fromCart: Bool
    @Binding var path: NavigationPath
    
    var body: some View {
        VStack {
            Text("商品ID: \(productId)")
            Text("来源: \(fromCart ? "购物车" : "普通浏览")")
        }
        .navigationTitle("商品详情")
    }
}

2.2 嵌套导航

swift

复制代码
// MARK: - 嵌套路由
struct NestedAppRoute: Hashable {
    enum Tab: Hashable {
        case home
        case profile
        case settings
    }
    
    var tab: Tab
    var path: [AppRoute]
}

struct NestedRouter: View {
    @State private var selectedTab: NestedAppRoute.Tab = .home
    @State private var homePath = NavigationPath()
    @State private var profilePath = NavigationPath()
    @State private var settingsPath = NavigationPath()
    
    var body: some View {
        TabView(selection: $selectedTab) {
            NavigationStack(path: $homePath) {
                HomeTabView(path: $homePath)
                    .navigationDestination(for: AppRoute.self) { route in
                        handleRoute(route, path: $homePath)
                    }
            }
            .tabItem {
                Label("首页", systemImage: "house")
            }
            .tag(NestedAppRoute.Tab.home)
            
            NavigationStack(path: $profilePath) {
                ProfileTabView(path: $profilePath)
                    .navigationDestination(for: AppRoute.self) { route in
                        handleRoute(route, path: $profilePath)
                    }
            }
            .tabItem {
                Label("个人", systemImage: "person")
            }
            .tag(NestedAppRoute.Tab.profile)
            
            NavigationStack(path: $settingsPath) {
                SettingsTabView(path: $settingsPath)
                    .navigationDestination(for: AppRoute.self) { route in
                        handleRoute(route, path: $settingsPath)
                    }
            }
            .tabItem {
                Label("设置", systemImage: "gear")
            }
            .tag(NestedAppRoute.Tab.settings)
        }
    }
    
    @ViewBuilder
    private func handleRoute(_ route: AppRoute, path: Binding<NavigationPath>) -> some View {
        switch route {
        case .userDetail(let id):
            UserDetailRouteView(userId: id, path: path)
        case .orderList(let userId):
            OrderListRouteView(userId: userId, path: path)
        default:
            EmptyView()
        }
    }
}

struct HomeTabView: View {
    @Binding var path: NavigationPath
    
    var body: some View {
        List {
            Button("用户详情") {
                path.append(AppRoute.userDetail(id: 1))
            }
        }
        .navigationTitle("首页")
    }
}

struct ProfileTabView: View {
    @Binding var path: NavigationPath
    
    var body: some View {
        List {
            Button("订单列表") {
                path.append(AppRoute.orderList(userId: 1))
            }
        }
        .navigationTitle("个人")
    }
}

struct SettingsTabView: View {
    @Binding var path: NavigationPath
    
    var body: some View {
        Text("设置页面")
            .navigationTitle("设置")
    }
}

三、协调器模式

3.1 协调器基础

swift

复制代码
// MARK: - 协调器协议
protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController? { get set }
    func start()
}

// MARK: - SwiftUI协调器
class AppCoordinator: ObservableObject {
    @Published var path = NavigationPath()
    private var dependencies: Dependencies
    
    struct Dependencies {
        let userService: UserServiceProtocol
        let orderService: OrderServiceProtocol
    }
    
    init(dependencies: Dependencies) {
        self.dependencies = dependencies
    }
    
    func navigate(to route: AppRoute) {
        path.append(route)
    }
    
    func pop() {
        path.removeLast()
    }
    
    func popToRoot() {
        path.removeLast(path.count)
    }
    
    @ViewBuilder
    func view(for route: AppRoute) -> some View {
        switch route {
        case .home:
            HomeCoordinatorView(coordinator: self)
        case .userDetail(let id):
            UserDetailCoordinatorView(coordinator: self, userId: id)
        case .orderList(let userId):
            OrderListCoordinatorView(coordinator: self, userId: userId)
        default:
            EmptyView()
        }
    }
}

// MARK: - 协调器视图
struct AppCoordinatorView: View {
    @StateObject var coordinator: AppCoordinator
    
    var body: some View {
        NavigationStack(path: $coordinator.path) {
            coordinator.view(for: .home)
                .navigationDestination(for: AppRoute.self) { route in
                    coordinator.view(for: route)
                }
        }
    }
}

struct HomeCoordinatorView: View {
    let coordinator: AppCoordinator
    
    var body: some View {
        List {
            Button("用户详情") {
                coordinator.navigate(to: .userDetail(id: 1))
            }
            Button("订单列表") {
                coordinator.navigate(to: .orderList(userId: 1))
            }
        }
        .navigationTitle("首页")
    }
}

struct UserDetailCoordinatorView: View {
    let coordinator: AppCoordinator
    let userId: Int
    
    var body: some View {
        VStack {
            Text("用户ID: \(userId)")
            Button("查看订单") {
                coordinator.navigate(to: .orderList(userId: userId))
            }
        }
        .navigationTitle("用户详情")
    }
}

struct OrderListCoordinatorView: View {
    let coordinator: AppCoordinator
    let userId: Int
    
    var body: some View {
        List {
            Text("订单列表 for user \(userId)")
        }
        .navigationTitle("订单列表")
    }
}

3.2 可测试性

swift

复制代码
// MARK: - 协议抽象
protocol Routing {
    func navigate(to route: AppRoute)
    func pop()
    func popToRoot()
}

class MockCoordinator: Routing {
    var navigatedRoutes: [AppRoute] = []
    var popCalled = false
    var popToRootCalled = false
    
    func navigate(to route: AppRoute) {
        navigatedRoutes.append(route)
    }
    
    func pop() {
        popCalled = true
    }
    
    func popToRoot() {
        popToRootCalled = true
    }
}

// MARK: - 可测试的视图
struct TestableHomeView: View {
    let router: Routing
    
    var body: some View {
        List {
            Button("用户详情") {
                router.navigate(to: .userDetail(id: 1))
            }
            Button("订单列表") {
                router.navigate(to: .orderList(userId: 1))
            }
        }
        .navigationTitle("首页")
    }
}

四、深度链接

4.1 URL路由解析

swift

复制代码
// MARK: - URL路由解析器
struct URLRouter {
    static let scheme = "myapp"
    
    static func parse(url: URL) -> AppRoute? {
        guard url.scheme == scheme else { return nil }
        
        let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
        let path = url.pathComponents
        
        switch url.host {
        case "user":
            if path.count > 1, let id = Int(path[1]) {
                return .userDetail(id: id)
            }
        case "order":
            if path.count > 1, let id = Int(path[1]) {
                return .orderDetail(orderId: id)
            }
        case "product":
            if path.count > 1, let id = Int(path[1]) {
                let fromCart = components?.queryItems?.first(where: { $0.name == "fromCart" })?.value == "true"
                return .productDetail(productId: id, fromCart: fromCart)
            }
        case "settings":
            return .settings
        case "about":
            return .about
        default:
            break
        }
        
        return nil
    }
    
    static func url(for route: AppRoute) -> URL {
        var components = URLComponents()
        components.scheme = scheme
        
        switch route {
        case .userDetail(let id):
            components.host = "user"
            return components.url!.appendingPathComponent("\(id)")
        case .orderDetail(let id):
            components.host = "order"
            return components.url!.appendingPathComponent("\(id)")
        case .productDetail(let id, let fromCart):
            components.host = "product"
            components.queryItems = [URLQueryItem(name: "fromCart", value: fromCart ? "true" : "false")]
            return components.url!.appendingPathComponent("\(id)")
        case .settings:
            components.host = "settings"
            return components.url!
        case .about:
            components.host = "about"
            return components.url!
        default:
            return components.url!
        }
    }
}

// MARK: - 深度链接处理
class DeepLinkManager: ObservableObject {
    @Published var pendingRoute: AppRoute?
    
    func handleURL(_ url: URL) -> Bool {
        if let route = URLRouter.parse(url: url) {
            pendingRoute = route
            return true
        }
        return false
    }
    
    func consumePendingRoute() -> AppRoute? {
        let route = pendingRoute
        pendingRoute = nil
        return route
    }
}

// MARK: - 支持深度链接的应用
struct DeepLinkAppView: View {
    @StateObject private var coordinator = AppCoordinator(dependencies: AppCoordinator.Dependencies(
        userService: UserService(),
        orderService: OrderService()
    ))
    @StateObject private var deepLinkManager = DeepLinkManager()
    
    var body: some View {
        AppCoordinatorView(coordinator: coordinator)
            .onOpenURL { url in
                if let route = URLRouter.parse(url: url) {
                    coordinator.navigate(to: route)
                }
            }
            .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
                guard let url = userActivity.webpageURL,
                      let route = URLRouter.parse(url: url) else { return }
                coordinator.navigate(to: route)
            }
    }
}

// 模拟服务
protocol UserServiceProtocol {}
protocol OrderServiceProtocol {}
struct UserService: UserServiceProtocol {}
struct OrderService: OrderServiceProtocol {}

4.2 推送通知导航

swift

复制代码
// MARK: - 推送通知路由
struct NotificationRouter {
    static func handleNotification(_ userInfo: [AnyHashable: Any]) -> AppRoute? {
        guard let type = userInfo["type"] as? String else { return nil }
        
        switch type {
        case "user":
            if let userId = userInfo["userId"] as? Int {
                return .userDetail(id: userId)
            }
        case "order":
            if let orderId = userInfo["orderId"] as? Int {
                return .orderDetail(orderId: orderId)
            }
        case "product":
            if let productId = userInfo["productId"] as? Int {
                let fromCart = userInfo["fromCart"] as? Bool ?? false
                return .productDetail(productId: productId, fromCart: fromCart)
            }
        default:
            break
        }
        
        return nil
    }
}

// MARK: - 通知处理扩展
extension Notification.Name {
    static let openRoute = Notification.Name("openRoute")
}

// 在AppDelegate中处理推送
class AppNotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        
        if let route = NotificationRouter.handleNotification(userInfo) {
            NotificationCenter.default.post(name: .openRoute, object: route)
        }
        
        completionHandler()
    }
}

五、状态恢复

5.1 场景状态保存

swift

复制代码
// MARK: - 可保存的导航状态
struct NavigationState: Codable {
    var routes: [String]
}

class StatefulCoordinator: ObservableObject {
相关推荐
黑化旺仔5 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb5 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙6 小时前
【iOS】swift启动屏显示黑屏
ios
用户69371750013848 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer8 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户69371750013849 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下10 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco13511 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记1 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921431 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview