Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
SwiftUI's navigation APIs provide data-driven, programmatic navigation that scales from simple stacks to complex multi-column layouts. Introduced in iOS 16 (2022) with NavigationStack and NavigationSplitView, evolved in iOS 18 (2024) with Tab/Sidebar unification, and refined in iOS 26 (2025) with Liquid Glass design.
NavigationView is deprecated as of iOS 16. Use NavigationStack (single-column push/pop) or NavigationSplitView (multi-column) exclusively in new code. Key improvements: single NavigationPath replaces per-link isActive bindings, value-based type safety, built-in Codable state restoration. See "Migrating to new navigation types" documentation.
NavigationStack Complete Reference
NavigationStack represents a push-pop interface like Settings on iPhone or System Settings on macOS.
NavigationPath is a type-erased collection for heterogeneous navigation stacks.
Typed Array vs NavigationPath
// Typed array: All values same type@Stateprivatevar path: [Recipe] = []
// NavigationPath: Mixed types@Stateprivatevar path =NavigationPath()
NavigationPath Operations
// Append value
path.append(recipe)
// Pop to previous
path.removeLast()
// Pop to root
path.removeLast(path.count)
// or
path =NavigationPath()
// Check countif path.count >0 { ... }
// Deep link: Set multiple values
path.append(category)
path.append(recipe)
Codable Support
// NavigationPath is Codable when all values are Codable@Stateprivatevar path =NavigationPath()
// Encodelet data =tryJSONEncoder().encode(path.codable)
// Decodelet codableRep =tryJSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data)
path =NavigationPath(codableRep)
NavigationSplitView Complete Reference
NavigationSplitView creates multi-column layouts that adapt to device size.
@Stateprivatevar columnVisibility: NavigationSplitViewVisibility= .all
NavigationSplitView(columnVisibility: $columnVisibility) {
Sidebar()
} content: {
Content()
} detail: {
Detail()
}
// Programmatically control visibility
columnVisibility = .detailOnly // Hide sidebar and content
columnVisibility = .all // Show all columns
columnVisibility = .automatic // System decides
2.5 Automatic Adaptation
NavigationSplitView automatically adapts:
iPad landscape All columns visible (depending on configuration)
iPad portrait/Slide Over Collapses to overlay or single column
iPhone Single navigation stack
Apple Watch/TV Single navigation stack
Selection changes automatically translate to push/pop on iPhone.
2.6 iOS 26+ Liquid Glass Sidebar (WWDC 2025, 323)
NavigationSplitView {
List { ... }
} detail: {
DetailView()
}
// Sidebar automatically gets Liquid Glass appearance on iPad/macOS// Extend content behind glass sidebar
.backgroundExtensionEffect() // Mirrors and blurs content outside safe area
Deep Linking and URL Routing
3.1 Deep Link Pattern
Use .onOpenURL to receive URLs, parse with URLComponents, then manipulate NavigationPath:
.onOpenURL { url inguardlet components =URLComponents(url: url, resolvingAgainstBaseURL: false),
let host = components.host else { return }
path.removeLast(path.count) // Pop to root first// Parse host/path to determine destination, then path.append(value)
}
For multi-step deep links (myapp://category/desserts/recipe/apple-pie), iterate URL path components and append each resolved value to build the full navigation stack.
For comprehensive deep linking examples, error diagnosis, and testing workflows, see axiom-swiftui-nav-diag (Pattern 3).
State Restoration
4.1 Complete State Restoration (WWDC 2022, 18:12)
structUseSceneStorage: View {
@StateObjectprivatevar navModel =NavigationModel()
@SceneStorage("navigation") privatevar data: Data?
@StateObjectprivatevar dataModel =DataModel()
var body: someView {
NavigationSplitView {
List(Category.allCases, selection: $navModel.selectedCategory) { category inNavigationLink(category.localizedName, value: category)
}
.navigationTitle("Categories")
} detail: {
NavigationStack(path: $navModel.recipePath) {
RecipeGrid(category: navModel.selectedCategory)
}
}
.task {
// Restore on appeariflet data = data {
navModel.jsonData = data
}
// Save on changesforawait_in navModel.objectWillChangeSequence {
data = navModel.jsonData
}
}
.environmentObject(dataModel)
}
}
4.2 Codable NavigationModel
classNavigationModel: ObservableObject, Codable {
@Publishedvar selectedCategory: Category?
@Publishedvar recipePath: [Recipe] = []
enumCodingKeys: String, CodingKey {
case selectedCategory
case recipePathIds // Store IDs, not full objects
}
funcencode(toencoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
}
init() {}
requiredinit(fromdecoder: Decoder) throws {
let container =try decoder.container(keyedBy: CodingKeys.self)
self.selectedCategory =try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
let recipePathIds =try container.decode([Recipe.ID].self, forKey: .recipePathIds)
self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] } // Discard deleted items
}
}
Store IDs (not full model objects) and use compactMap to handle deleted items gracefully. Add jsonData computed property and objectWillChangeSequence for SceneStorage integration as shown in 4.1.
Search tab requirement: Contents of a search-role tab must be wrapped in NavigationStack with .searchable() applied to the stack. Without NavigationStack, the search field will not appear. For foundational .searchable patterns (suggestions, scopes, tokens, programmatic control), see axiom-swiftui-search-ref.
.contextMenu on Tab only applies to the sidebar representation (iPad/Mac). iPhone tab bar context menus require UIKit interop (adding UILongPressGestureRecognizer to UITabBar via Introspect or a UITabBarController subclass). See axiom-swiftui-nav-diag for workaround patterns.
Caveat: Relies on private UITabBarButton subviews — fragile across iOS versions, not a public API guarantee.
5.6 Programmatic Tab Visibility
Use .hidden(_:) to show/hide tabs based on app state while preserving their navigation state.
State-Driven Tab Visibility
Tab("Libraries", systemImage: "square.stack") { LibrariesView() }
.hidden(context == .browse) // Hide based on app state
Apply .hidden(condition) to each tab. Tabs hidden this way preserve their navigation state (unlike conditional if rendering which destroys and recreates them).
State Preservation
Key difference: .hidden(_:) preserves tab state, conditional rendering does not.
// ✅ State preserved when hiddenTab("Settings", systemImage: "gear") {
SettingsView() // Navigation stack preserved
}
.hidden(!showSettings)
// ❌ State lost when condition changesif showSettings {
Tab("Settings", systemImage: "gear") {
SettingsView() // Navigation stack recreated
}
}
Same pattern applies to authentication state, purchase status, and debug builds — bind .hidden() to any boolean condition.
Animated Transitions
Wrap state changes in withAnimation for smooth tab bar layout transitions:
Button("Switch to Browse") {
withAnimation {
context = .browse
selection = .tracks // Switch to first visible tab
}
}
// Tab bar animates as tabs appear/disappear// Uses system motion curves automatically
5.7 iOS 26+ Tab Features (WWDC 2025, 256)
// Tab bar minimization on scrollTabView { ... }
.tabBarMinimizeBehavior(.onScrollDown)
// Bottom accessory view (always visible)TabView { ... }
.tabViewBottomAccessory {
PlaybackControls()
}
// Dynamic visibility (recommended for mini-players)// ⚠️ Requires iOS 26.1+ (not 26.0)TabView { ... }
.tabViewBottomAccessory(isEnabled: showMiniPlayer) {
MiniPlayerView()
.transition(.opacity)
}
// isEnabled: true = shows accessory// isEnabled: false = hides AND removes reserved space// Search tab with dedicated search fieldTab(role: .search) {
NavigationStack {
SearchView()
.navigationTitle("Search")
}
.searchable(text: $searchText)
}
// Morphs into search field when selected// ⚠️ NavigationStack wrapper required for search field to appear// Fallback: If no tab has .search role, the tab view applies search// to ALL tabs, resetting search state when the selected tab changes
Dynamic Bottom Accessory
The accessory can switch on activeTab for per-tab content, though Apple's usage (Music mini-player) keeps it global. Read @Environment(\.tabViewBottomAccessoryPlacement) to adapt layout: .bar when above tab bar (full controls), other values when inline with collapsed tab bar (compact).
Reserve tabViewBottomAccessory for cross-tab content (playback, status). For tab-specific actions, prefer floating glass buttons within the tab's content view.
Foundational search APIs For .searchable, isSearching, suggestions, scopes, tokens, and programmatic control, see axiom-swiftui-search-ref. This section covers iOS 26 bottom-aligned refinement only.
NavigationSplitView {
Sidebar()
} detail: {
DetailView()
}
.searchable(text: $query, prompt: "What are you looking for?")
// Automatically bottom-aligned on iPhone, top-trailing on iPad
6.4 Scroll Edge Effect
// Automatic blur effect when content scrolls under toolbar// Remove any custom darkening backgrounds - they interfere// For dense UIs, adjust sharpnessScrollView { ... }
.scrollEdgeEffectStyle(.soft) // .sharp, .soft
6.5 Sheet Presentations with Zoom Transition
In iOS 26, sheets can morph directly out of the buttons that present them. Make the presenting toolbar item a source for a navigation zoom transition, and mark the sheet content as the destination:
Other presentations also flow smoothly out of Liquid Glass controls — menus, alerts, and popovers. Dialogs automatically morph out of the buttons that present them without additional code.
Audit tip: If you've used presentationBackground to apply custom backgrounds to sheets, consider removing it and let the new Liquid Glass sheet material shine. Partial height sheets are now inset with glass background by default.
6.6 Toolbar Morphing Transitions
iOS 26 automatically morphs toolbars during NavigationStack push/pop when each destination view declares its own .toolbar {}. Items with matching toolbar(id:) and ToolbarItem(id:) IDs stay stable during the transition (no bounce), while unmatched items animate in/out.
Key rule: Attach .toolbar {} to individual views inside NavigationStack, not to NavigationStack itself. Otherwise there is nothing to morph between.
See axiom-swiftui-26-ref skill for complete toolbar morphing API including DefaultToolbarItem, toolbar(id:) stable items, ToolbarSpacer patterns, and troubleshooting.
Router/Coordinator Patterns
7.1 When to Use Coordinators
Use coordinators when:
Navigation logic is complex with conditional flows
Testing navigation in isolation
Sharing navigation logic across multiple screens
UIKit interop with heavy navigation requirements
Use built-in navigation when:
Simple linear or hierarchical navigation
State restoration is primary concern
Fewer than 5-10 navigation destinations
No need for navigation unit testing
7.2 Simple Router Pattern
// Route enum defines all possible destinationsenumAppRoute: Hashable {
case home
case category(Category)
case recipe(Recipe)
case settings
}
// Router class manages navigation@ObservableclassRouter {
var path =NavigationPath()
funcnavigate(toroute: AppRoute) {
path.append(route)
}
funcpopToRoot() {
path.removeLast(path.count)
}
funcpop() {
if!path.isEmpty {
path.removeLast()
}
}
}
// Usage in viewsstructContentView: View {
@Stateprivatevar router =Router()
var body: someView {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: AppRoute.self) { route inswitch route {
case .home:
HomeView()
case .category(let category):
CategoryView(category: category)
case .recipe(let recipe):
RecipeDetail(recipe: recipe)
case .settings:
SettingsView()
}
}
}
.environment(router)
}
}
// In child viewsstructRecipeCard: View {
let recipe: Recipe@Environment(Router.self) privatevar router
var body: someView {
Button(recipe.name) {
router.navigate(to: .recipe(recipe))
}
}
}
7.3 Coordinator Pattern with Protocol
For larger apps, extract a Coordinator protocol with associatedtype Route: Hashable and var path: NavigationPath. Each feature area gets its own coordinator conformance with domain-specific routes and convenience methods (e.g., showRecipeOfTheDay() that resets path and navigates).
7.4 Testing Navigation
// Router is easily testablefunctestNavigateToRecipe() {
let router =Router()
let recipe =Recipe(name: "Apple Pie")
router.navigate(to: .recipe(recipe))
XCTAssertEqual(router.path.count, 1)
}
functestPopToRoot() {
let router =Router()
router.navigate(to: .category(.desserts))
router.navigate(to: .recipe(Recipe(name: "Apple Pie")))
router.popToRoot()
XCTAssertTrue(router.path.isEmpty)
}
Testing Checklist
Deep links navigate correctly from cold start AND while running
Pop to root clears entire stack
State restores on app relaunch (SceneStorage key unique per scene)
Deleted items handled gracefully in restoration (compactMap)
NavigationSplitView collapses correctly on iPhone (selection pushes)
iOS 26+: Liquid Glass appearance, bottom-aligned search, tab bar minimization
Last Updated Based on WWDC 2022-10054, WWDC 2024-10147, WWDC 2025-256, WWDC 2025-323 (Build a SwiftUI app with the new design)
Platforms iOS 16+, iPadOS 16+, macOS 13+, watchOS 9+, tvOS 16+