swiftui-navigation

swiftui-navigation

熱門

實作 SwiftUI 導航模式,包括 NavigationStack、NavigationSplitView、sheet 呈現、分頁導航與深層連結。適用於建構推播導航、程式化路由、多欄佈局、模態 sheet、分頁列、通用連結或自訂 URL scheme 處理。

931星標
0分支
更新於 2026/7/26
SKILL.md
readonlyread-only
name
swiftui-navigation
description

實作 SwiftUI 導航模式,包括 NavigationStack、NavigationSplitView、sheet 呈現、分頁導航與深層連結。適用於建構推播導航、程式化路由、多欄佈局、模態 sheet、分頁列、通用連結或自訂 URL scheme 處理。

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("項目")
        }
    }
}

程式化導航:

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("項目")
        } detail: {
            if let item = selectedItem {
                ItemDetailView(item: item)
            } else {
                ContentUnavailableView("選取一個項目", 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("首頁", systemImage: "house", value: .home) {
                NavigationStack { HomeView() }
            }
            Tab("搜尋", systemImage: "magnifyingglass", value: .search) {
                NavigationStack { SearchView() }
            }
            Tab("個人檔案", 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

深層連結

使用解析 → 驗證 → 提交的流程。先解析為型別路由而不改變導航;驗證 scheme/host/path、識別碼格式、授權與目的地是否存在;然後原子地更新分頁/路徑。無效連結必須保持當前導航不變。

通用連結

通用連結讓 iOS 可以為標準 HTTPS URL 開啟你的應用程式。它們需要:

  1. /.well-known/apple-app-site-association 放置 Apple App Site Association (AASA) 檔案
  2. 一個關聯網域授權(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.plistCFBundleURLTypes 下註冊 scheme。使用 .onOpenURL 處理。對於公開分享的連結,優先使用通用連結而非自訂 scheme——它們提供網頁備援與網域驗證。

Handoff(NSUserActivity)

使用 .userActivity() 宣傳活動,並使用 .onContinueUserActivity() 接收 Handoff 或其他使用者活動。在 Info.plistNSUserActivityTypes 下宣告活動類型。設定 isEligibleForHandoff = true 並提供 webpageURL 作為備援。

完整的 AASA 配置、路由器 URL 處理、自訂 URL scheme 與 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:)

參考資料