关于 swiftui-navigation
为使用 Swift 6.3 构建 iOS 应用的开发者整理 SwiftUI 导航模式,涵盖推送导航、多栏布局、工作表、标签页和深层链接,并提供路由组织与验证建议。
主要功能
- 01类型安全的 NavigationStack 路由
- 02多栏与自定义分栏布局
- 03工作表呈现及路由
- 04独立标签页导航架构
- 05深层链接解析与验证
- 06导航审查清单
适用场景
- 01为复杂应用按标签维护独立导航栈
- 02为 iPad 和 Mac 构建主从界面
- 03根据所选模型呈现编辑工作表
- 04验证链接后原子更新应用导航
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftui-navigation安装后在 Claude Code 中输入/swiftui-navigation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/deeplinks.mdnavigationstack.mdsheets.mdtabview.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 打开你的应用。需要:
- 在
/.well-known/apple-app-site-association放置 Apple App Site Association(AASA)文件 - 配置 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 延续的完整示例。
常见错误
- 使用已弃用的
NavigationView——改用NavigationStack或NavigationSplitView - 在所有标签页间共用一个导航路径或路由器——每个标签页都需要独立的路径
- 当状态表示一个模型时使用
.sheet(isPresented:)——应改用.sheet(item:) - 在导航路径中存储视图实例——应存储轻量级的
Hashable路由数据 - 将
@Observable路由器对象嵌套在其他@Observable对象中 - 优先使用
Tab(value:)搭配TabView(selection:),而非较旧的.tabItem { }API - 误以为
tabBarMinimizeBehavior在 iPad 上可用——它仅适用于 iPhone - 在多个位置处理深度链接——应将 URL 解析集中到路由器中
- 将表单尺寸写死为固定值——应改用
.presentationSizing(.form) - 路由器类缺少
@MainActor——Swift 6 并发安全要求添加此属性
检查清单
- 使用
NavigationStack(而非NavigationView) - 每个标签页都有独立路径的
NavigationStack - 路由枚举为
Hashable,且标识符稳定 -
.navigationDestination(for:)映射所有路由类型 - 优先使用
.sheet(item:),而非.sheet(isPresented:) - 工作表在内部处理自身的关闭逻辑
- 路由器对象标记为
@MainActor和@Observable - 导航前解析并验证深度链接 URL
- 已配置通用链接的 AASA 和 Associated Domains
- 标签页选择使用带绑定的
Tab(value:)
参考资料
- NavigationStack 和路由模式:references/navigationstack.md
- Sheet 展示与路由:references/sheets.md
- TabView 模式和 iOS 26 API:references/tabview.md
- 深层链接、通用链接和 Handoff:references/deeplinks.md
- 架构和状态管理:参见
swiftui-patterns技能 - 布局和组件:参见
swiftui-layout-components技能