swiftui-navigation by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-navigation适用于 iOS 26+ 和 Swift 6.2 的 SwiftUI 应用导航模式。涵盖推送导航、多列布局、表单展示、标签页架构和深度链接。除非特别说明,这些模式向下兼容至 iOS 17。
使用带有 NavigationPath 绑定的 NavigationStack 来实现编程化、类型安全的推送导航。将路由定义为 Hashable 枚举,并使用 .navigationDestination(for:) 进行映射。
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")
}
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
编程化导航:
path.append(item) // 推送
path.removeLast() // 弹出一个
path = NavigationPath() // 弹出到根视图
路由模式: 对于具有复杂导航的应用,使用一个拥有路径和表单状态的路由器对象。每个标签页通过 .environment() 注入其自己的路由器实例。使用单个 .navigationDestination(for:) 代码块或共享的 withAppRouter() 修饰符来集中管理目标映射。
有关完整路由器示例,包括每个标签页的独立堆栈、集中式目标映射和通用标签页路由,请参阅 references/navigationstack.md。
在 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")
}
}
}
}
对于自定义的多列布局(例如,独立于选择项的专用通知列),使用带有 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 -- 比页面稍窄,适用于表单式用户界面.fitted -- 根据内容的理想尺寸调整大小微调:.fitted(horizontal:vertical:) 约束适配的轴;.sticky(horizontal:vertical:) 在指定维度上增长但不收缩。
关闭确认 (macOS 15+ / iOS 26+): 使用 .dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges) 来防止意外关闭包含未保存更改的表单。
枚举驱动的表单路由: 定义一个符合 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() }
}
}
}
}
带有副作用的自定义绑定: 通过一个函数来路由选择更改,以拦截应触发操作而非改变选择的特殊标签页(例如,撰写)。
Tab(role: .search) -- 激活时用搜索字段替换标签栏.tabBarMinimizeBehavior(_:) -- .onScrollDown, .onScrollUp, .never (仅限 iPhone).tabViewSidebarHeader/Footer -- 在 iPadOS/macOS 上自定义侧边栏部分.tabViewBottomAccessory { } -- 在标签栏下方附加内容(例如,正在播放栏)TabSection -- 使用 .tabPlacement(.sidebarOnly) 将标签页分组到侧边栏部分中完整的 TabView 模式,包括自定义绑定、动态标签页和侧边栏定制,请参阅 references/tabview.md。
通用链接允许 iOS 为标准的 HTTPS URL 打开您的应用。它们需要:
/.well-known/apple-app-site-association 路径下有一个 Apple App Site Association (AASA) 文件applinks:example.com)在 SwiftUI 中使用 .onOpenURL 和 .onContinueUserActivity 处理:
@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)
}
}
}
}
在 Info.plist 的 CFBundleURLTypes 下注册方案。使用 .onOpenURL 处理。对于公开分享的链接,优先使用通用链接而非自定义方案——它们提供网页回退和域名验证。
使用 .userActivity() 发布活动,并使用 .onContinueUserActivity() 接收它们。在 Info.plist 的 NSUserActivityTypes 下声明活动类型。设置 isEligibleForHandoff = true 并提供 webpageURL 作为回退。
完整的 AASA 配置、路由器 URL 处理、自定义 URL 方案和 NSUserActivity 延续示例,请参阅 references/deeplinks.md。
NavigationView -- 使用 NavigationStack 或 NavigationSplitViewNavigationPath -- 每个标签页都需要自己的路径.sheet(isPresented:) -- 应改用 .sheet(item:)NavigationPath 中存储视图实例 -- 应存储轻量级的 Hashable 路由数据@Observable 路由器对象嵌套在其他 @Observable 对象内部.tabItem { } API -- 应使用带有 TabView(selection:) 的 Tab(value:)tabBarMinimizeBehavior 在 iPad 上有效 -- 它仅适用于 iPhone.presentationSizing(.form)@MainActor -- Swift 6 并发安全性所必需NavigationStack(而非 NavigationView)NavigationStack 和独立路径Hashable 的,并具有稳定的标识符.navigationDestination(for:) 映射了所有路由类型.sheet(item:) 而非 .sheet(isPresented:)@MainActor 和 @Observable 的Tab(value:)references/navigationstack.mdreferences/sheets.mdreferences/tabview.mdreferences/deeplinks.mdswiftui-patterns 技能swiftui-layout-components 技能每周安装次数
355
代码仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex349
github-copilot347
amp346
cline346
kimi-cli346
gemini-cli346
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.
Use NavigationStack with a NavigationPath binding for programmatic, type-safe push navigation. Define routes as a Hashable enum and map them with .navigationDestination(for:).
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:
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.
Use NavigationSplitView for sidebar-detail layouts on iPad and Mac. Falls back to stack navigation on 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")
}
}
}
}
For custom multi-column layouts (e.g., a dedicated notification column independent of selection), use a manual HStack split with horizontalSizeClass checks:
@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.
Prefer .sheet(item:) over .sheet(isPresented:) when state represents a selected model. Sheets should own their actions and call dismiss() internally.
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}
Presentation sizing (iOS 18+): Control sheet dimensions with .presentationSizing:
.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 sizeFine-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.
Use the Tab API with a selection binding for scalable tab architecture. Each tab should wrap its content in an independent 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() }
}
}
}
}
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(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.
Universal links let iOS open your app for standard HTTPS URLs. They require:
/.well-known/apple-app-site-associationapplinks:example.com)Handle in SwiftUI with .onOpenURL and .onContinueUserActivity:
@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)
}
}
}
}
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.
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.
NavigationView -- use NavigationStack or NavigationSplitViewNavigationPath across all tabs -- each tab needs its own path.sheet(isPresented:) when state represents a model -- use .sheet(item:) insteadNavigationPath -- store lightweight Hashable route data@Observable router objects inside other @Observable objectsNavigationStack used (not NavigationView)NavigationStack with independent pathHashable with stable identifiers.navigationDestination(for:) maps all route types.sheet(item:) preferred over .sheet(isPresented:)@MainActor and @Observablereferences/navigationstack.mdreferences/sheets.mdreferences/tabview.mdreferences/deeplinks.mdswiftui-patterns skillswiftui-layout-components skillWeekly Installs
355
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex349
github-copilot347
amp346
cline346
kimi-cli346
gemini-cli346
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
.tabItem { } API -- use Tab(value:) with TabView(selection:)tabBarMinimizeBehavior works on iPad -- it is iPhone only.presentationSizing(.form) instead@MainActor on router classes -- required for Swift 6 concurrency safetyTab(value:) with binding