第六章:iOS导航与路由系统

本章覆盖 iOS 17 的完整导航体系:NavigationStack(栈式导航)、TabView(Tab导航)、Sheet/FullScreenCover(弹出导航)、NavigationSplitView(分栏导航),以及 Deep Link 和 Universal Links 深链接处理。


NavigationStack 是 iOS 16+ 推荐的导航方式,支持程序化控制整个导航栈。

swift 复制代码
// App 根导航结构
struct AppRootView: View {
    // NavigationPath 管理导航栈状态(可序列化、可程序化控制)
    @State private var navigationPath = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            HomeView()
                // 注册路由目标(类型安全)
                .navigationDestination(for: Article.self) { article in
                    ArticleDetailView(article: article)
                }
                .navigationDestination(for: User.self) { user in
                    UserProfileView(user: user)
                }
                .navigationDestination(for: AppRoute.self) { route in
                    AppRouteView(route: route)
                }
        }
        .environment(\.navigationPath, $navigationPath)  // 暴露给子视图
    }
}

// 路由枚举(将路由集中管理)
enum AppRoute: Hashable {
    case settings
    case notifications
    case search(String)
    case articleDetail(String)
    case userProfile(String)
}

// 在任意视图中使用 NavigationLink
struct HomeView: View {
    let articles: [Article] = Article.samples
    
    var body: some View {
        List(articles) { article in
            // 声明式导航:点击自动 push
            NavigationLink(value: article) {
                ArticleRowView(article: article)
            }
        }
        .navigationTitle("首页")
        .navigationBarTitleDisplayMode(.large)
    }
}

// 程序化导航(从 ViewModel 或按钮触发跳转)
struct NavigationProgrammaticDemo: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack(spacing: 20) {
                Button("跳转到设置") {
                    path.append(AppRoute.settings)   // push
                }
                
                Button("跳转到搜索") {
                    path.append(AppRoute.search("Swift"))
                }
                
                Button("直达二级页面") {
                    // 一次 push 多层(跳过中间层)
                    path.append(AppRoute.settings)
                    path.append(AppRoute.notifications)
                }
                
                Button("回到根页面") {
                    path.removeLast(path.count)   // pop to root
                }
            }
            .navigationTitle("导航演示")
            .navigationDestination(for: AppRoute.self) { route in
                AppRouteView(route: route)
            }
        }
    }
}
swift 复制代码
struct CustomNavBarDemo: View {
    var body: some View {
        List { /* 内容 */ }
        .navigationTitle("自定义导航栏")
        .navigationBarTitleDisplayMode(.inline)  // .large / .inline / .automatic
        
        // 工具栏按钮
        .toolbar {
            // 左侧按钮
            ToolbarItem(placement: .topBarLeading) {
                Button("取消") { }
            }
            
            // 右侧单按钮
            ToolbarItem(placement: .topBarTrailing) {
                Button { } label: {
                    Image(systemName: "plus")
                }
            }
            
            // 右侧多按钮(菜单)
            ToolbarItem(placement: .topBarTrailing) {
                Menu {
                    Button("按时间排序") { }
                    Button("按热度排序") { }
                    Divider()
                    Button("筛选", role: .destructive) { }
                } label: {
                    Image(systemName: "ellipsis.circle")
                }
            }
            
            // 底部工具栏
            ToolbarItem(placement: .bottomBar) {
                HStack {
                    Button("分享") { }
                    Spacer()
                    Button("下载") { }
                }
            }
        }
        
        // 搜索栏
        .searchable(text: .constant(""), placement: .navigationBarDrawer,
                    prompt: "搜索文章")
        
        // 隐藏返回按钮
        .navigationBarBackButtonHidden(true)
        // 自定义返回按钮
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                BackButton()
            }
        }
    }
}

6.2 TabView - Tab 导航

swift 复制代码
struct MainTabView: View {
    @State private var selectedTab: Tab = .home
    
    enum Tab: String, CaseIterable {
        case home = "首页"
        case explore = "发现"
        case publish = "发布"
        case message = "消息"
        case profile = "我的"
        
        var icon: String {
            switch self {
            case .home: "house.fill"
            case .explore: "safari.fill"
            case .publish: "plus.circle.fill"
            case .message: "message.fill"
            case .profile: "person.fill"
            }
        }
    }
    
    var body: some View {
        TabView(selection: $selectedTab) {
            ForEach(Tab.allCases, id: \.self) { tab in
                NavigationStack {
                    tabContent(for: tab)
                }
                .tabItem {
                    Label(tab.rawValue, systemImage: tab.icon)
                }
                .tag(tab)
                // 角标
                .badge(badgeCount(for: tab))
            }
        }
        .tint(.blue)  // 选中颜色
    }
    
    @ViewBuilder
    func tabContent(for tab: Tab) -> some View {
        switch tab {
        case .home:     HomeView()
        case .explore:  ExploreView()
        case .publish:  PublishView()
        case .message:  MessageView()
        case .profile:  ProfileView()
        }
    }
    
    func badgeCount(for tab: Tab) -> Int {
        switch tab {
        case .message: return 5
        default: return 0
        }
    }
}

// 自定义 Tab 外观(iOS 18 新 API,iOS 17 需 UITabBar 定制)
struct StyledTabView: View {
    var body: some View {
        TabView {
            Tab("首页", systemImage: "house") { HomeView() }
            Tab("消息", systemImage: "message") { MessageView() }
                .badge(3)
            Tab("我的", systemImage: "person") { ProfileView() }
        }
        .tabViewStyle(.automatic)
    }
}

6.3 Sheet 与弹出导航

swift 复制代码
struct SheetNavigationDemo: View {
    @State private var showAddSheet = false
    @State private var showLoginFullScreen = false
    @State private var selectedArticle: Article?
    
    var body: some View {
        VStack(spacing: 20) {
            // ① Sheet - 支持多段高度(iOS 16+)
            Button("添加内容(Sheet)") { showAddSheet = true }
            .sheet(isPresented: $showAddSheet) {
                AddContentView()
                    .presentationDetents([
                        .height(200),           // 固定高度
                        .fraction(0.5),         // 屏幕 50%
                        .medium,                // 约 50%
                        .large                  // 全屏
                    ])
                    .presentationDragIndicator(.visible)
                    .presentationCornerRadius(24)
                    // 不拦截后面的视图交互
                    .presentationBackgroundInteraction(.enabled(upThrough: .medium))
            }
            
            // ② Sheet with item(有关联数据时弹出)
            Button("查看文章") { selectedArticle = Article.samples.first }
            .sheet(item: $selectedArticle) { article in
                ArticleReaderView(article: article)
                    .presentationDetents([.large])
            }
            
            // ③ FullScreenCover - 全屏弹出
            Button("登录(全屏)") { showLoginFullScreen = true }
            .fullScreenCover(isPresented: $showLoginFullScreen) {
                LoginView(onSuccess: { showLoginFullScreen = false })
            }
        }
    }
}

// Sheet 内部视图关闭自己
struct AddContentView: View {
    @Environment(\.dismiss) var dismiss  // 获取 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("完成") {
                        save()
                        dismiss()
                    }
                    .disabled(title.isEmpty)
                }
            }
        }
    }
    
    func save() { /* 保存逻辑 */ }
}

swift 复制代码
// 三栏布局(iPad / macOS)
struct SplitViewLayout: View {
    @State private var selectedCategory: Category?
    @State private var selectedArticle: Article?
    @State private var columnVisibility = NavigationSplitViewVisibility.automatic
    
    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            // 侧边栏(左):分类列表
            List(Category.allCases, selection: $selectedCategory) { category in
                Label(category.name, systemImage: category.icon)
            }
            .navigationTitle("分类")
        } content: {
            // 内容栏(中):文章列表
            if let category = selectedCategory {
                ArticleListColumn(category: category, selection: $selectedArticle)
            } else {
                ContentUnavailableView("选择分类", systemImage: "sidebar.left")
            }
        } detail: {
            // 详情栏(右):文章内容
            if let article = selectedArticle {
                ArticleDetailView(article: article)
            } else {
                ContentUnavailableView("选择文章", systemImage: "doc.text")
            }
        }
        .navigationSplitViewStyle(.balanced)
    }
}

swift 复制代码
// Info.plist 配置 URL Scheme
// CFBundleURLSchemes: ["iosDemo"]

@main
struct iOSDemosApp: App {
    @State private var appRouter = AppRouter()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(appRouter)
                // 处理 URL Scheme 深链接
                .onOpenURL { url in
                    appRouter.handle(url: url)
                }
                // 处理 Universal Links(需要 Associated Domains 配置)
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    if let url = activity.webpageURL {
                        appRouter.handle(url: url)
                    }
                }
        }
    }
}

// 路由处理器
@Observable
class AppRouter {
    var navigationPath = NavigationPath()
    var presentedSheet: AppRoute?
    
    func handle(url: URL) {
        // iosDemo://article/123
        // iosDemo://user/profile/456
        // https://yourdomain.com/article/123
        
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            return
        }
        
        let pathComponents = components.path
            .components(separatedBy: "/")
            .filter { !$0.isEmpty }
        
        switch pathComponents.first {
        case "article":
            if let id = pathComponents.dropFirst().first {
                navigationPath.append(AppRoute.articleDetail(id))
            }
        case "user", "profile":
            if let userId = pathComponents.last {
                navigationPath.append(AppRoute.userProfile(userId))
            }
        case "settings":
            navigationPath.append(AppRoute.settings)
        default:
            break
        }
    }
    
    func navigate(to route: AppRoute) {
        navigationPath.append(route)
    }
    
    func popToRoot() {
        navigationPath.removeLast(navigationPath.count)
    }
}

章节总结

导航类型 API 适用场景
栈式导航 NavigationStack 层级页面(列表→详情)
程序化控制 NavigationPath ViewModel 触发导航、深链接
Tab 导航 TabView 顶级功能区切换
底部弹出 .sheet + presentationDetents 半屏/全屏内容弹出
全屏弹出 .fullScreenCover 登录、引导页
分栏导航 NavigationSplitView iPad/Mac 自适应布局
深链接 .onOpenURL 外部唤起 App 特定页面

Demo 说明

文件 演示内容
NavigationStackDemo.swift 程序化导航、NavigationPath
TabViewDemo.swift 主 Tab 结构 + 角标
SheetNavigationDemo.swift Sheet 多段高度 + FullScreenCover
SplitViewDemo.swift iPad 三栏分栏布局
DeepLinkDemo.swift URL Scheme 深链接处理

📎 扩展内容补充

来源:第六章_主题与国际化.md
本章概述:掌握 iOS 的 Dark Mode 适配、自定义颜色系统、动态颜色、以及多语言国际化方案(String Catalog)。


6.1 Dark Mode 与主题系统

概念讲解

动态颜色适配
swift 复制代码
// 在 Assets.xcassets 中定义自适应颜色(Any/Dark 两套)
// 代码中使用:
Color("AppPrimary")           // 自动根据深浅色切换
Color("BackgroundColor")

// 系统语义颜色(推荐,自动适配 Dark Mode)
Color.primary           // 主要文本色
Color.secondary         // 次要文本色
Color.background        // 背景色(白/黑)
Color(.systemGroupedBackground)
Color(.label)           // 标签色
Color(.systemBlue)      // 系统蓝

// 读取当前模式
struct ThemeAwareView: View {
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        VStack {
            Text("当前模式:\(colorScheme == .dark ? "深色" : "浅色")")
            
            RoundedRectangle(cornerRadius: 16)
                .fill(colorScheme == .dark ? Color(.systemGray5) : .white)
                .shadow(
                    color: colorScheme == .dark ? .clear : .black.opacity(0.1),
                    radius: 8
                )
        }
    }
}
自定义主题系统
swift 复制代码
// 定义 App 主题(类比 Flutter 的 ThemeData)
struct AppTheme {
    let primaryColor: Color
    let secondaryColor: Color
    let backgroundColor: Color
    let surfaceColor: Color
    let textPrimary: Color
    let textSecondary: Color
    let cornerRadius: CGFloat
    let shadowRadius: CGFloat
    
    static let light = AppTheme(
        primaryColor: Color(hex: "#007AFF"),
        secondaryColor: Color(hex: "#5AC8FA"),
        backgroundColor: Color(.systemBackground),
        surfaceColor: .white,
        textPrimary: Color(.label),
        textSecondary: Color(.secondaryLabel),
        cornerRadius: 12,
        shadowRadius: 8
    )
    
    static let dark = AppTheme(
        primaryColor: Color(hex: "#0A84FF"),
        secondaryColor: Color(hex: "#64D2FF"),
        backgroundColor: Color(.systemBackground),
        surfaceColor: Color(.systemGray6),
        textPrimary: Color(.label),
        textSecondary: Color(.secondaryLabel),
        cornerRadius: 12,
        shadowRadius: 0
    )
}

// 通过 EnvironmentKey 注入主题
struct AppThemeKey: EnvironmentKey {
    static let defaultValue = AppTheme.light
}

extension EnvironmentValues {
    var appTheme: AppTheme {
        get { self[AppThemeKey.self] }
        set { self[AppThemeKey.self] = newValue }
    }
}

// 在 App 根部注入
@main
struct iOSDemosApp: App {
    @Environment(\.colorScheme) var colorScheme
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.appTheme, 
                              colorScheme == .dark ? AppTheme.dark : AppTheme.light)
        }
    }
}

// 子视图使用主题
struct ThemedButton: View {
    @Environment(\.appTheme) var theme
    let title: String
    let action: () -> Void
    
    var body: some View {
        Button(action: action) {
            Text(title)
                .fontWeight(.semibold)
                .foregroundStyle(.white)
                .padding(.horizontal, 24)
                .padding(.vertical, 12)
                .background(theme.primaryColor)
                .cornerRadius(theme.cornerRadius)
        }
    }
}
颜色扩展
swift 复制代码
extension Color {
    // 十六进制颜色
    init(hex: String) {
        let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
        var int: UInt64 = 0
        Scanner(string: hex).scanHexInt64(&int)
        let a, r, g, b: UInt64
        switch hex.count {
        case 3:
            (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
        case 6:
            (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
        case 8:
            (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
        default:
            (a, r, g, b) = (1, 1, 1, 0)
        }
        self.init(
            .sRGB,
            red: Double(r) / 255,
            green: Double(g) / 255,
            blue: Double(b) / 255,
            opacity: Double(a) / 255
        )
    }
}

6.2 国际化(多语言)

概念讲解

iOS 17 推荐使用 String Catalog(.xcstrings 文件),比传统 Localizable.strings 更易维护。

String Catalog 使用方式
swift 复制代码
// 在代码中直接写中文,Xcode 自动提取到 .xcstrings
Text("欢迎使用")           // 自动国际化的键
Text("items_count \(count)", tableName: nil, bundle: nil, comment: "")

// 使用带参数的本地化字符串
Text("welcome_user \(username)")

// 对应 String Catalog 中:
// "welcome_user %@" -> 中文:"欢迎,%@!"
//                   -> 英文:"Welcome, %@!"
传统 Localizable.strings 方式
swift 复制代码
// zh-Hans.lproj/Localizable.strings
// "home_title" = "首页";
// "settings_title" = "设置";
// "welcome_user %@" = "欢迎,%@!";

// en.lproj/Localizable.strings
// "home_title" = "Home";
// "settings_title" = "Settings";
// "welcome_user %@" = "Welcome, %@!";

// 代码中使用
Text(String(localized: "home_title"))
Text("home_title")  // SwiftUI Text 自动查找本地化

// 带参数
let message = String(localized: "welcome_user \(username)")
Text(message)
动态切换语言
swift 复制代码
@Observable
class LocalizationManager {
    var currentLanguage: String = Locale.current.language.languageCode?.identifier ?? "zh"
    
    private static let supportedLanguages = ["zh-Hans", "en", "ja"]
    var supportedLanguages: [Language] = [
        Language(code: "zh-Hans", displayName: "简体中文"),
        Language(code: "en", displayName: "English"),
        Language(code: "ja", displayName: "日本語")
    ]
    
    func setLanguage(_ code: String) {
        currentLanguage = code
        UserDefaults.standard.set([code], forKey: "AppleLanguages")
        // 需要重启 App 生效(或使用第三方库实现即时切换)
    }
}

struct LanguageSettingsView: View {
    @Environment(LocalizationManager.self) var locManager
    
    var body: some View {
        List(locManager.supportedLanguages, id: \.code) { language in
            HStack {
                Text(language.displayName)
                Spacer()
                if locManager.currentLanguage == language.code {
                    Image(systemName: "checkmark")
                        .foregroundStyle(.blue)
                }
            }
            .contentShape(Rectangle())
            .onTapGesture {
                locManager.setLanguage(language.code)
            }
        }
        .navigationTitle("语言设置")
    }
}
日期、数字、货币格式化
swift 复制代码
struct FormattingDemo: View {
    let date = Date()
    let price = 1234.56
    let count = 1000000
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // 日期格式化(自动根据语言环境格式化)
            Text(date, format: .dateTime.year().month().day())
            Text(date, format: .dateTime.hour().minute())
            Text(date.formatted(.relative(presentation: .named)))  // "2天前"
            
            // 数字格式化
            Text(count, format: .number)              // 1,000,000
            Text(price, format: .number.precision(.fractionLength(2)))
            
            // 货币格式化
            Text(price, format: .currency(code: "CNY"))  // ¥1,234.56
            Text(price, format: .currency(code: "USD"))  // $1,234.56
            
            // 文件大小
            Text(1024 * 1024, format: .byteCount(style: .file))  // 1 MB
        }
    }
}

章节总结

知识点 关键点 重要程度
Dark Mode 适配 使用系统语义颜色 / Assets 动态颜色 ⭐⭐⭐⭐⭐
自定义主题 EnvironmentKey 注入 AppTheme ⭐⭐⭐⭐
String Catalog iOS 17 推荐方案 ⭐⭐⭐⭐
格式化 日期/数字/货币自动随语言变化 ⭐⭐⭐⭐

Demo 说明

Demo 文件 演示内容
DarkModeDemo.swift Dark Mode 切换 + 自定义主题系统
LocalizationDemo.swift String Catalog + 动态语言切换
相关推荐
空中海1 天前
第十一章:iOS性能优化、测试与发布
ios·性能优化
iAnMccc1 天前
Swift Codable 的 5 个生产环境陷阱,以及如何优雅地解决它们
ios
iAnMccc1 天前
从 HandyJSON 迁移到 SmartCodable:我们团队的实践
ios
kerli1 天前
基于 kmp/cmp 的跨平台图片加载方案 - 适配 Android View/Compose/ios
android·前端·ios
懋学的前端攻城狮1 天前
第三方SDK集成沉思录:在便捷与可控间寻找平衡
ios·前端框架
冰凌时空1 天前
Swift vs Objective-C:语言设计哲学的全面对比
ios·openai
花间相见1 天前
【大模型微调与部署03】—— ms-swift-3.12 命令行参数(训练、推理、对齐、量化、部署全参数)
开发语言·ios·swift
SameX1 天前
删掉ML推荐、砍掉五时段分析——做专注App时我三次推翻自己,换来了什么
ios
爱吃香蕉的阿豪1 天前
Mac 远程操作 Windows 开发:ZeroTier + JetBrains 实战指南
windows·macos·zerotoer
YJlio1 天前
2026年4月19日60秒读懂世界:从学位扩容到人形机器人夺冠,今天最值得关注的6个信号
python·安全·ios·机器人·word·iphone·7-zip