swiftui-navigation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Navigation

SwiftUI 导航

Navigation patterns for SwiftUI apps targeting iOS 26+ with Swift 6.2. Covers push navigation, multi-column layouts, sheet presentation, tab architecture, and deep linking. Patterns are backward-compatible to iOS 17 unless noted.
本文针对适配iOS 26+、使用Swift 6.2的SwiftUI应用,介绍各类导航模式,包括推送导航、多列布局、弹窗展示、标签页架构以及深度链接。除非另有说明,这些模式向后兼容至iOS 17。

Contents

目录

NavigationStack (Push Navigation)

NavigationStack(推送导航)

Use
NavigationStack
with a
NavigationPath
binding for programmatic, type-safe push navigation. Define routes as a
Hashable
enum and map them with
.navigationDestination(for:)
.
swift
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                }
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
            .navigationTitle("Items")
        }
    }
}
Programmatic navigation:
swift
path.append(item)        // Push
path.removeLast()        // Pop one
path = NavigationPath()  // Pop to root
Router pattern: For apps with complex navigation, use a router object that owns the path and sheet state. Each tab gets its own router instance injected via
.environment()
. Centralize destination mapping with a single
.navigationDestination(for:)
block or a shared
withAppRouter()
modifier.
See
references/navigationstack.md
for full router examples including per-tab stacks, centralized destination mapping, and generic tab routing.
结合
NavigationStack
NavigationPath
绑定,实现程序化、类型安全的推送导航。将路由定义为
Hashable
枚举,通过
.navigationDestination(for:)
进行映射。
swift
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                }
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
            .navigationTitle("Items")
        }
    }
}
程序化导航:
swift
path.append(item)        // 推送页面
path.removeLast()        // 返回上一页
path = NavigationPath()  // 返回根页面
路由模式: 对于导航逻辑复杂的应用,可使用路由对象管理路径和弹窗状态。通过
.environment()
为每个标签页注入独立的路由实例。通过单个
.navigationDestination(for:)
代码块或共享的
withAppRouter()
修饰符集中管理目标页面映射。
完整的路由示例(包括多标签页独立栈、集中式目标映射、通用标签页路由)可参考
references/navigationstack.md

NavigationSplitView (Multi-Column)

NavigationSplitView(多列布局)

Use
NavigationSplitView
for sidebar-detail layouts on iPad and Mac. Falls back to stack navigation on iPhone.
swift
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")
            }
        }
    }
}
在iPad和Mac上使用
NavigationSplitView
实现侧边栏-详情页布局,在iPhone上会自动回退为栈式导航。
swift
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")
            }
        }
    }
}

Custom Split Column (Manual HStack)

自定义分栏布局(手动HStack实现)

For custom multi-column layouts (e.g., a dedicated notification column independent of selection), use a manual
HStack
split with
horizontalSizeClass
checks:
swift
@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 { /* tabs */ }
  }

  private var secondaryColumn: some View {
    NotificationsTab()
      .environment(\.isSecondaryColumn, true)
      .frame(maxWidth: .secondaryColumnWidth)
  }
}
Use the manual HStack split when you need full control or a non-standard secondary column. Use
NavigationSplitView
when you want a standard system layout with minimal customization.
如果需要自定义多列布局(例如,独立于选中项的通知栏),可结合
horizontalSizeClass
判断,通过手动
HStack
实现分栏:
swift
@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 Presentation

弹窗展示

Prefer
.sheet(item:)
over
.sheet(isPresented:)
when state represents a selected model. Sheets should own their actions and call
dismiss()
internally.
swift
@State private var selectedItem: Item?

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
}
Presentation sizing (iOS 18+): Control sheet dimensions with
.presentationSizing
:
swift
.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .presentationSizing(.form)  // .form, .page, .fitted, .automatic
}
PresentationSizing
values:
  • .automatic
    -- platform default
  • .page
    -- roughly paper size, for informational content
  • .form
    -- slightly narrower than page, for form-style UI
  • .fitted
    -- sized by the content's ideal size
Fine-tuning:
.fitted(horizontal:vertical:)
constrains fitting axes;
.sticky(horizontal:vertical:)
grows but does not shrink in specified dimensions.
Dismissal confirmation (macOS 15+ / iOS 26+): Use
.dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges)
to prevent accidental dismissal of sheets with unsaved changes.
Enum-driven sheet routing: Define a
SheetDestination
enum that is
Identifiable
, store it on the router, and map it with a shared view modifier. This lets any child view present sheets without prop-drilling. See
references/sheets.md
for the full centralized sheet routing pattern.
当状态对应一个选中模型时,优先使用
.sheet(item:)
而非
.sheet(isPresented:)
。弹窗应自行管理关闭逻辑,在内部调用
dismiss()
swift
@State private var selectedItem: Item?

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
}
弹窗尺寸控制(iOS 18+): 通过
.presentationSizing
控制弹窗尺寸:
swift
.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:)
可让弹窗在指定轴方向上仅放大不缩小。
关闭确认(macOS 15+ / iOS 26+): 使用
.dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges)
防止用户意外关闭存在未保存修改的弹窗。
枚举驱动的弹窗路由: 定义遵循
Identifiable
协议的
SheetDestination
枚举,将其存储在路由对象中,通过共享视图修饰符进行映射。这样子视图无需层层传递状态即可触发弹窗展示。完整的集中式弹窗路由模式可参考
references/sheets.md

Tab-Based Navigation

基于标签页的导航

Use the
Tab
API with a selection binding for scalable tab architecture. Each tab should wrap its content in an independent
NavigationStack
.
swift
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() }
            }
        }
    }
}
Custom binding with side effects: Route selection changes through a function to intercept special tabs (e.g., compose) that should trigger an action instead of changing selection.
结合
Tab
API与选中状态绑定,实现可扩展的标签页架构。每个标签页应将内容包裹在独立的
NavigationStack
中。
swift
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 Additions

iOS 26 标签页新增特性

  • Tab(role: .search)
    -- replaces the tab bar with a search field when active
  • .tabBarMinimizeBehavior(_:)
    --
    .onScrollDown
    ,
    .onScrollUp
    ,
    .never
    (iPhone only)
  • .tabViewSidebarHeader/Footer
    -- customize sidebar sections on iPadOS/macOS
  • .tabViewBottomAccessory { }
    -- attach content below the tab bar (e.g., Now Playing bar)
  • TabSection
    -- group tabs into sidebar sections with
    .tabPlacement(.sidebarOnly)
See
references/tabview.md
for full TabView patterns including custom bindings, dynamic tabs, and sidebar customization.
  • Tab(role: .search)
    -- 选中时将标签栏替换为搜索框
  • .tabBarMinimizeBehavior(_:)
    -- 可选值:
    .onScrollDown
    ,
    .onScrollUp
    ,
    .never
    (仅iPhone支持)
  • .tabViewSidebarHeader/Footer
    -- 在iPadOS/macOS上自定义侧边栏的头部/尾部
  • .tabViewBottomAccessory { }
    -- 在标签栏下方附加内容(例如,正在播放栏)
  • TabSection
    -- 通过
    .tabPlacement(.sidebarOnly)
    将标签页分组为侧边栏中的不同板块
完整的TabView模式(包括自定义绑定、动态标签页、侧边栏自定义)可参考
references/tabview.md

Deep Links

深度链接

Universal Links

通用链接

Universal links let iOS open your app for standard HTTPS URLs. They require:
  1. An Apple App Site Association (AASA) file at
    /.well-known/apple-app-site-association
  2. An Associated Domains entitlement (
    applinks:example.com
    )
Handle in SwiftUI with
.onOpenURL
and
.onContinueUserActivity
:
swift
@main
struct MyApp: App {
    @State private var router = Router()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(router)
                .onOpenURL { url in router.handle(url: url) }
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    guard let url = activity.webpageURL else { return }
                    router.handle(url: url)
                }
        }
    }
}
通用链接可让iOS通过标准HTTPS URL直接打开你的应用,配置要求:
  1. /.well-known/apple-app-site-association
    路径下放置Apple App Site Association(AASA)文件
  2. 配置Associated Domains权限(
    applinks:example.com
在SwiftUI中通过
.onOpenURL
.onContinueUserActivity
处理:
swift
@main
struct MyApp: App {
    @State private var router = Router()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(router)
                .onOpenURL { url in router.handle(url: url) }
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    guard let url = activity.webpageURL else { return }
                    router.handle(url: url)
                }
        }
    }
}

Custom URL Schemes

自定义URL协议

Register schemes in
Info.plist
under
CFBundleURLTypes
. Handle with
.onOpenURL
. Prefer universal links over custom schemes for publicly shared links -- they provide web fallback and domain verification.
Info.plist
CFBundleURLTypes
中注册自定义协议,通过
.onOpenURL
处理。对于公开分享的链接,优先使用通用链接而非自定义协议——通用链接支持网页降级和域名验证。

Handoff (NSUserActivity)

接力功能(NSUserActivity)

Advertise activities with
.userActivity()
and receive them with
.onContinueUserActivity()
. Declare activity types in
Info.plist
under
NSUserActivityTypes
. Set
isEligibleForHandoff = true
and provide a
webpageURL
as fallback.
See
references/deeplinks.md
for full examples of AASA configuration, router URL handling, custom URL schemes, and NSUserActivity continuation.
通过
.userActivity()
发布活动,通过
.onContinueUserActivity()
接收活动。在
Info.plist
NSUserActivityTypes
中声明活动类型。设置
isEligibleForHandoff = true
并提供
webpageURL
作为降级方案。
完整的示例(包括AASA配置、路由URL处理、自定义URL协议、NSUserActivity接力)可参考
references/deeplinks.md

Common Mistakes

常见错误

  1. Using deprecated
    NavigationView
    -- use
    NavigationStack
    or
    NavigationSplitView
  2. Sharing one
    NavigationPath
    across all tabs -- each tab needs its own path
  3. Using
    .sheet(isPresented:)
    when state represents a model -- use
    .sheet(item:)
    instead
  4. Storing view instances in
    NavigationPath
    -- store lightweight
    Hashable
    route data
  5. Nesting
    @Observable
    router objects inside other
    @Observable
    objects
  6. Using deprecated
    .tabItem { }
    API -- use
    Tab(value:)
    with
    TabView(selection:)
  7. Assuming
    tabBarMinimizeBehavior
    works on iPad -- it is iPhone only
  8. Handling deep links in multiple places -- centralize URL parsing in the router
  9. Hard-coding sheet frame dimensions -- use
    .presentationSizing(.form)
    instead
  10. Missing
    @MainActor
    on router classes -- required for Swift 6 concurrency safety
  1. 使用已废弃的
    NavigationView
    ——应使用
    NavigationStack
    NavigationSplitView
  2. 所有标签页共享同一个
    NavigationPath
    ——每个标签页需要独立的路径
  3. 当状态对应模型时使用
    .sheet(isPresented:)
    ——应改用
    .sheet(item:)
  4. NavigationPath
    中存储视图实例——应存储轻量的
    Hashable
    路由数据
  5. 在其他
    @Observable
    对象中嵌套
    @Observable
    路由对象
  6. 使用已废弃的
    .tabItem { }
    API——应结合绑定使用
    Tab(value:)
    TabView(selection:)
  7. 认为
    tabBarMinimizeBehavior
    在iPad上可用——该特性仅支持iPhone
  8. 在多个位置处理深度链接——应在路由中集中解析URL
  9. 硬编码弹窗尺寸——应使用
    .presentationSizing(.form)
    替代
  10. 路由类未添加
    @MainActor
    ——Swift 6并发安全要求必须添加

Review Checklist

审核检查清单

  • NavigationStack
    used (not
    NavigationView
    )
  • Each tab has its own
    NavigationStack
    with independent path
  • Route enum is
    Hashable
    with stable identifiers
  • .navigationDestination(for:)
    maps all route types
  • .sheet(item:)
    preferred over
    .sheet(isPresented:)
  • Sheets own their dismiss logic internally
  • Router object is
    @MainActor
    and
    @Observable
  • Deep link URLs parsed and validated before navigation
  • Universal links have AASA and Associated Domains configured
  • Tab selection uses
    Tab(value:)
    with binding
  • 使用了
    NavigationStack
    (而非
    NavigationView
  • 每个标签页拥有独立的
    NavigationStack
    和路径
  • 路由枚举遵循
    Hashable
    协议且具备稳定的标识符
  • .navigationDestination(for:)
    映射了所有路由类型
  • 优先使用
    .sheet(item:)
    而非
    .sheet(isPresented:)
  • 弹窗在内部管理关闭逻辑
  • 路由对象添加了
    @MainActor
    @Observable
  • 深度链接URL在导航前经过解析和验证
  • 通用链接已配置AASA文件和Associated Domains权限
  • 标签页选中逻辑使用了带绑定的
    Tab(value:)

References

参考资料

  • NavigationStack and router patterns:
    references/navigationstack.md
  • Sheet presentation and routing:
    references/sheets.md
  • TabView patterns and iOS 26 API:
    references/tabview.md
  • Deep links, universal links, and Handoff:
    references/deeplinks.md
  • Architecture and state management: see
    swiftui-patterns
    skill
  • Layout and components: see
    swiftui-layout-components
    skill
  • NavigationStack与路由模式:
    references/navigationstack.md
  • 弹窗展示与路由:
    references/sheets.md
  • TabView模式与iOS 26 API:
    references/tabview.md
  • 深度链接、通用链接与接力功能:
    references/deeplinks.md
  • 架构与状态管理:参考
    swiftui-patterns
    技能文档
  • 布局与组件:参考
    swiftui-layout-components
    技能文档