mcpField
S
swiftui-navigation

swiftui-navigation

github.com/dpearson2699开发1.2k4.4k 次安装13 周前更新
代码仓库

关于 swiftui-navigation

为使用 Swift 6.3 构建 iOS 应用的开发者整理 SwiftUI 导航模式,涵盖推送导航、多栏布局、工作表、标签页和深层链接,并提供路由组织与验证建议。

主要功能

  1. 01类型安全的 NavigationStack 路由
  2. 02多栏与自定义分栏布局
  3. 03工作表呈现及路由
  4. 04独立标签页导航架构
  5. 05深层链接解析与验证
  6. 06导航审查清单

适用场景

  1. 01为复杂应用按标签维护独立导航栈
  2. 02为 iPad 和 Mac 构建主从界面
  3. 03根据所选模型呈现编辑工作表
  4. 04验证链接后原子更新应用导航

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-navigation

安装后在 Claude Code 中输入/swiftui-navigation确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
deeplinks.md
navigationstack.md
sheets.md
tabview.md

详细介绍

SwiftUI 导航

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

目录

NavigationStack(推送导航)

使用 NavigationStack 和类型化的 [Route] 绑定实现程序化推送导航。将路由定义为 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))  // Push
path.removeLast()                // Pop one
path = []                        // Pop to root

路由器模式: 对于导航复杂的应用,使用一个路由器对象来管理路径和工作表状态。每个标签页都通过 .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 { /* tabs */ }
  }

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

当你需要完全控制布局或使用非标准的次级列时,使用手动 HStack 分栏。若希望采用自定义程度最低的标准系统布局,则使用 NavigationSplitView。

工作表呈现

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

@State private var selectedItem: Item?

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

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

.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),并在工作表内提供明确的保存/放弃操作。在 macOS 15+ 上,使用 .dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges) 确认关闭窗口。 所有程序化关闭都应经过相同的保存/验证/放弃流程; interactiveDismissDisabled 仅阻止交互式关闭。

由枚举驱动的工作表路由: 定义一个遵循 Identifiable 的 SheetDestination 枚举,将其存储在路由器上,并使用共享视图修饰符进行映射。这样,任何子视图都能呈现工作表,无需逐层传递属性。完整的集中式工作表路由模式请参阅 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) 使用

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

深度链接

使用“解析 → 验证 → 提交”的流程。将链接解析为类型化路由,不要修改导航状态;验证 scheme、host、path、标识符格式、授权和目标是否存在;然后原子性地更新标签页和路径。无效链接不得改变当前导航状态。

通用链接

通用链接允许 iOS 使用标准 HTTPS URL 打开你的应用。需要:

  1. 在 /.well-known/apple-app-site-association 放置 Apple App Site Association(AASA)文件
  2. 配置 Associated Domains 权限(applinks:example.com)

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

@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 Scheme

在 Info.plist 的 CFBundleURLTypes 中注册 scheme。使用 .onOpenURL 处理。对于公开分享的链接,优先使用通用链接,而非自定义 scheme——通用链接提供网页回退和域名验证。

Handoff(NSUserActivity)

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

请参阅 references/deeplinks.md,查看 AASA 配置、路由器 URL 处理、自定义 URL scheme 和 NSUserActivity 延续的完整示例。

常见错误

  1. 使用已弃用的 NavigationView——改用 NavigationStack 或 NavigationSplitView
  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. 将表单尺寸写死为固定值——应改用 .presentationSizing(.form)
  10. 路由器类缺少 @MainActor——Swift 6 并发安全要求添加此属性

检查清单

  • 使用 NavigationStack(而非 NavigationView)
  • 每个标签页都有独立路径的 NavigationStack
  • 路由枚举为 Hashable,且标识符稳定
  • .navigationDestination(for:) 映射所有路由类型
  • 优先使用 .sheet(item:),而非 .sheet(isPresented:)
  • 工作表在内部处理自身的关闭逻辑
  • 路由器对象标记为 @MainActor 和 @Observable
  • 导航前解析并验证深度链接 URL
  • 已配置通用链接的 AASA 和 Associated Domains
  • 标签页选择使用带绑定的 Tab(value:)

参考资料

评论

支持 Markdown