从零开始学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 {
相关推荐
MonkeyKing715516 小时前
iOS 开发 ARC 与 MRC 底层原理及区别
ios·面试
唐诺18 小时前
iOS 与 Xcode 版本差异指南
ios·cocoa·xcode
MonkeyKing1 天前
iOS dyld加载流程与App启动原理(pre-main阶段)详解
ios
MonkeyKing1 天前
iOS类加载全解析:map_images、load_images、initialize调用时机
ios
美狐美颜SDK开放平台1 天前
什么是美颜SDK?高并发场景下的企业级美颜SDK如何开发?
android·人工智能·ios·美颜sdk·第三方美颜sdk·视频美颜sdk
90后的晨仔1 天前
SwiftUI 数据持久化完全指南:从偏好设置到企业级存储
ios·axios
90后的晨仔1 天前
SwiftUI 高级特性第3章:环境与偏好设置
ios
Digitally1 天前
如何将短信从 iPhone 传输到 Mac?
macos·ios·iphone
MonkeyKing71551 天前
iOS 开发 UIView 与 CALayer 关系及渲染流程
ios·面试