swiftui-navigation

swiftui-navigation

热门

实现 SwiftUI 导航模式,包括 NavigationStack、NavigationSplitView、sheet 呈现、标签导航和深度链接。适用于构建推送导航、程序化路由、多列布局、模态 sheet、标签栏、通用链接或自定义 URL 方案处理。

931Star
0Fork
更新于 2026/7/26
SKILL.md
readonly只读
name
swiftui-navigation
description

实现 SwiftUI 导航模式,包括 NavigationStack、NavigationSplitView、sheet 呈现、标签导航和深度链接。适用于构建推送导航、程序化路由、多列布局、模态 sheet、标签栏、通用链接或自定义 URL 方案处理。

SwiftUI 导航

适用于 iOS 26+ 和 Swift 6.3 的 SwiftUI 应用导航模式。涵盖推送导航、多列布局、sheet 呈现、标签架构和深度链接。除非另有说明,这些模式向后兼容至 iOS 17。

目录

NavigationStack(推送导航)

使用带有类型化 [Route] 绑定的 NavigationStack 进行程序化推送导航。将路由定义为 Hashable 枚举,并使用 .navigationDestination(for:) 映射;这使路径在编译时得到检查。仅当单个栈必须容纳异构路由值类型时,才使用 NavigationPath

enum Route: Hashable {
    case item(id: Item.ID)
}

struct ContentView: View {
    @State private var path: [Route] = []
    let items: [Item]

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: Route.item(id: item.id)) {
                    ItemRow(item: item)
                }
            }
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .item(let id):
                    DetailView(itemID: id)
                }
            }
            .navigationTitle("Items")
        }
    }
}

程序化导航:

path.append(.item(id: item.id))  // 推送
path.removeLast()                // 弹出
path = []                        // 返回根

路由器模式: 对于具有复杂导航的应用,使用一个拥有路径和 sheet 状态的路由器对象。每个标签通过 .environment() 注入自己的路由器实例。使用单个 .navigationDestination(for:) 块或共享的 withAppRouter() 修饰符集中目标映射。

有关完整的路由器示例,包括每个标签的栈、集中目标映射和通用标签路由,请参阅 references/navigationstack.md

NavigationSplitView(多列)

在 iPad 和 Mac 上使用 NavigationSplitView 实现侧边栏-详情布局。在 iPhone 上回退到栈导航。

struct MasterDetailView: View {
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView {
            List(items, selection: $selectedItem) { item in
                NavigationLink(value: item) { ItemRow(item: item) }
            }
            .navigationTitle("Items")
        } detail: {
            if let item = selectedItem {
                ItemDetailView(item: item)
            } else {
                ContentUnavailableView("Select an Item", systemImage: "sidebar.leading")
            }
        }
    }
}

自定义分割列(手动 HStack)

对于自定义多列布局(例如,独立于选择的专用通知列),使用带有 horizontalSizeClass 检查的手动 HStack 分割:

@MainActor
struct AppView: View {
  @Environment(\.horizontalSizeClass) private var horizontalSizeClass
  @AppStorage("showSecondaryColumn") private var showSecondaryColumn = true

  var body: some View {
    HStack(spacing: 0) {
      primaryColumn
      if shouldShowSecondaryColumn {
        Divider().edgesIgnoringSafeArea(.all)
        secondaryColumn
      }
    }
  }

  private var shouldShowSecondaryColumn: Bool {
    horizontalSizeClass == .regular
      && showSecondaryColumn
  }

  private var primaryColumn: some View {
    TabView { /* 标签 */ }
  }

  private var secondaryColumn: some View {
    NotificationsTab()
      .environment(\.isSecondaryColumn, true)
      .frame(maxWidth: .secondaryColumnWidth)
  }
}

当需要完全控制或非标准辅助列时,使用手动 HStack 分割。当需要标准系统布局且最小自定义时,使用 NavigationSplitView

Sheet 呈现

当状态表示选中的模型时,优先使用 .sheet(item:) 而不是 .sheet(isPresented:)。Sheet 应拥有自己的操作并在内部调用 dismiss()

@State private var selectedItem: Item?

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
}

呈现尺寸(iOS 18+): 使用 .presentationSizing 控制 sheet 尺寸:

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .presentationSizing(.form)  // .form, .page, .fitted, .automatic
}

PresentationSizing 值:

  • .automatic -- 平台默认
  • .page -- 大致纸张大小,用于信息内容
  • .form -- 比 page 稍窄,用于表单样式 UI
  • .fitted -- 由内容的理想大小决定

微调:.fitted(horizontal:vertical:) 约束适配轴;.sticky(horizontal:vertical:) 在指定维度上增长但不缩小。

关闭保护: 在 iOS/iPadOS 上,使用 .interactiveDismissDisabled(hasUnsavedChanges) 并在 sheet 内部提供明确的保存/放弃操作。在 macOS 15+ 上,使用 .dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges) 进行窗口关闭确认。所有程序化关闭都通过相同的保存/验证/放弃门控;interactiveDismissDisabled 仅保护交互式关闭。

枚举驱动的 sheet 路由: 定义一个 IdentifiableSheetDestination 枚举,将其存储在路由器上,并使用共享视图修饰符映射。这允许任何子视图呈现 sheet 而无需属性传递。有关完整的集中式 sheet 路由模式,请参阅 references/sheets.md

标签导航

使用带有选择绑定的 Tab API 实现可扩展的标签架构。每个标签应将其内容包装在独立的 NavigationStack 中。

struct MainTabView: View {
    @State private var selectedTab: AppTab = .home

    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("Home", systemImage: "house", value: .home) {
                NavigationStack { HomeView() }
            }
            Tab("Search", systemImage: "magnifyingglass", value: .search) {
                NavigationStack { SearchView() }
            }
            Tab("Profile", systemImage: "person", value: .profile) {
                NavigationStack { ProfileView() }
            }
        }
    }
}

带副作用的自定义绑定: 通过函数路由选择更改,以拦截应触发操作而非更改选择的特殊标签(例如,撰写)。

iOS 26 标签新增功能

  • Tab(value:role:).search -- 标记专用搜索标签,具有系统默认搜索标题、图标和固定行为
  • .tabViewSearchActivation(_:) -- 控制搜索标签的激活和停用行为
  • .tabBarMinimizeBehavior(_:) -- .onScrollDown, .onScrollUp, .never(仅 iPhone)
  • .tabViewSidebarHeader/Footer -- 在 iPadOS/macOS 上自定义侧边栏部分
  • .tabViewBottomAccessory { } -- 在标签栏下方附加内容(例如,正在播放栏)
  • TabSection -- 使用 .tabPlacement(.sidebarOnly) 将标签分组到侧边栏部分

有关完整的 TabView 模式,包括自定义绑定、动态标签和侧边栏自定义,请参阅 references/tabview.md

深度链接

使用 解析 → 验证 → 提交。将 URL 解析为类型化路由而不改变导航;验证方案/主机/路径、标识符形状、授权和目标存在性;然后原子地更新标签/路径。无效链接必须保持当前导航不变。

通用链接

通用链接允许 iOS 为标准 HTTPS URL 打开您的应用。它们需要:

  1. /.well-known/apple-app-site-association 放置 Apple App Site Association (AASA) 文件
  2. 关联域授权(applinks:example.com

在 SwiftUI 中使用 .onOpenURL 处理通用链接和自定义 URL 方案:

@main
struct MyApp: App {
    @State private var router = Router()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(router)
                .onOpenURL { url in router.handle(url: url) }
        }
    }
}

自定义 URL 方案

Info.plistCFBundleURLTypes 下注册方案。使用 .onOpenURL 处理。对于公开共享的链接,优先使用通用链接而非自定义方案——它们提供 Web 回退和域验证。

Handoff(NSUserActivity)

使用 .userActivity() 宣传活动,并使用 .onContinueUserActivity() 接收 Handoff 或其他用户活动。在 Info.plistNSUserActivityTypes 下声明活动类型。设置 isEligibleForHandoff = true 并提供 webpageURL 作为回退。

有关 AASA 配置、路由器 URL 处理、自定义 URL 方案和 NSUserActivity 延续的完整示例,请参阅 references/deeplinks.md

常见错误

  1. 使用已弃用的 NavigationView -- 改用 NavigationStackNavigationSplitView
  2. 在所有标签间共享一个导航路径或路由器 -- 每个标签需要自己的路径
  3. 当状态表示模型时使用 .sheet(isPresented:) -- 改用 .sheet(item:)
  4. 在导航路径中存储视图实例 -- 存储轻量级的 Hashable 路由数据
  5. @Observable 路由器对象嵌套在其他 @Observable 对象中
  6. 优先使用 Tab(value:)TabView(selection:) 而非旧的 .tabItem { } API
  7. 假设 tabBarMinimizeBehavior 在 iPad 上有效 -- 它仅适用于 iPhone
  8. 在多个地方处理深度链接 -- 在路由器中集中 URL 解析
  9. 硬编码 sheet 框架尺寸 -- 改用 .presentationSizing(.form)
  10. 路由器类缺少 @MainActor -- Swift 6 并发安全需要

审查清单

  • [ ] 使用 NavigationStack(而非 NavigationView
  • [ ] 每个标签拥有自己的 NavigationStack 和独立路径
  • [ ] 路由枚举是 Hashable 且具有稳定标识符
  • [ ] .navigationDestination(for:) 映射所有路由类型
  • [ ] 优先使用 .sheet(item:) 而非 .sheet(isPresented:)
  • [ ] Sheet 在内部拥有自己的关闭逻辑
  • [ ] 路由器对象是 @MainActor@Observable
  • [ ] 深度链接 URL 在导航前经过解析和验证
  • [ ] 通用链接已配置 AASA 和关联域
  • [ ] 标签选择使用带有绑定的 Tab(value:)

参考资料