Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Design iOS mobile app UI/UX following Apple Human Interface Guidelines and SwiftUI/UIKit patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/ios-navigation.md
1# iOS Navigation Patterns23## NavigationStack (iOS 16+)45### Basic Navigation67```swift8struct BasicNavigationView: View {9var body: some View {10NavigationStack {11List(items) { item in12NavigationLink(item.title, value: item)13}14.navigationTitle("Items")15.navigationDestination(for: Item.self) { item in16ItemDetailView(item: item)17}18}19}20}21```2223### Programmatic Navigation2425```swift26struct ProgrammaticNavigationView: View {27@State private var path = NavigationPath()2829var body: some View {30NavigationStack(path: $path) {31VStack(spacing: 20) {32Button("Go to Settings") {33path.append(Destination.settings)34}3536Button("Go to Profile") {37path.append(Destination.profile)38}3940Button("Deep Link to Item 123") {41path.append(Destination.settings)42path.append(Destination.itemDetail(id: 123))43}44}45.navigationTitle("Home")46.navigationDestination(for: Destination.self) { destination in47switch destination {48case .settings:49SettingsView()50case .profile:51ProfileView()52case .itemDetail(let id):53ItemDetailView(itemId: id)54}55}56}57}5859enum Destination: Hashable {60case settings61case profile62case itemDetail(id: Int)63}64}65```6667### Navigation State Persistence6869```swift70struct PersistentNavigationView: View {71@SceneStorage("navigationPath") private var pathData: Data?72@State private var path = NavigationPath()7374var body: some View {75NavigationStack(path: $path) {76ContentView()77.navigationDestination(for: Item.self) { item in78ItemDetailView(item: item)79}80}81.onAppear {82restorePath()83}84.onChange(of: path) { _, newPath in85savePath(newPath)86}87}8889private func savePath(_ path: NavigationPath) {90guard let representation = path.codable else { return }91pathData = try? JSONEncoder().encode(representation)92}9394private func restorePath() {95guard let data = pathData,96let representation = try? JSONDecoder().decode(97NavigationPath.CodableRepresentation.self,98from: data99) else { return }100path = NavigationPath(representation)101}102}103```104105## NavigationSplitView106107### Two-Column Layout108109```swift110struct TwoColumnView: View {111@State private var selectedCategory: Category?112@State private var columnVisibility: NavigationSplitViewVisibility = .all113114var body: some View {115NavigationSplitView(columnVisibility: $columnVisibility) {116// Sidebar117List(categories, selection: $selectedCategory) { category in118NavigationLink(value: category) {119Label(category.name, systemImage: category.icon)120}121}122.navigationTitle("Categories")123} detail: {124// Detail125if let category = selectedCategory {126CategoryDetailView(category: category)127} else {128ContentUnavailableView(129"Select a Category",130systemImage: "sidebar.leading"131)132}133}134.navigationSplitViewStyle(.balanced)135}136}137```138139### Three-Column Layout140141```swift142struct ThreeColumnView: View {143@State private var selectedFolder: Folder?144@State private var selectedDocument: Document?145146var body: some View {147NavigationSplitView {148// Sidebar149List(folders, selection: $selectedFolder) { folder in150NavigationLink(value: folder) {151Label(folder.name, systemImage: "folder")152}153}154.navigationTitle("Folders")155} content: {156// Content column157if let folder = selectedFolder {158List(folder.documents, selection: $selectedDocument) { document in159NavigationLink(value: document) {160DocumentRow(document: document)161}162}163.navigationTitle(folder.name)164} else {165Text("Select a folder")166}167} detail: {168// Detail column169if let document = selectedDocument {170DocumentDetailView(document: document)171} else {172ContentUnavailableView(173"Select a Document",174systemImage: "doc"175)176}177}178}179}180```181182## Sheet Navigation183184### Modal Sheets185186```swift187struct SheetNavigationView: View {188@State private var showSettings = false189@State private var showNewItem = false190@State private var editingItem: Item?191192var body: some View {193NavigationStack {194ContentView()195.toolbar {196ToolbarItem(placement: .primaryAction) {197Button("Add", systemImage: "plus") {198showNewItem = true199}200}201ToolbarItem(placement: .topBarLeading) {202Button("Settings", systemImage: "gear") {203showSettings = true204}205}206}207}208// Boolean-based sheet209.sheet(isPresented: $showSettings) {210SettingsSheet()211}212// Boolean-based fullscreen cover213.fullScreenCover(isPresented: $showNewItem) {214NewItemView()215}216// Item-based sheet217.sheet(item: $editingItem) { item in218EditItemSheet(item: item)219}220}221}222```223224### Sheet with Navigation225226```swift227struct NavigableSheet: View {228@Environment(\.dismiss) private var dismiss229230var body: some View {231NavigationStack {232Form {233Section("General") {234NavigationLink("Account") {235AccountSettingsView()236}237NavigationLink("Notifications") {238NotificationSettingsView()239}240}241242Section("Advanced") {243NavigationLink("Privacy") {244PrivacySettingsView()245}246}247}248.navigationTitle("Settings")249.toolbar {250ToolbarItem(placement: .confirmationAction) {251Button("Done") {252dismiss()253}254}255}256}257}258}259```260261### Sheet Customization262263```swift264struct CustomSheetView: View {265@State private var showSheet = false266267var body: some View {268Button("Show Sheet") {269showSheet = true270}271.sheet(isPresented: $showSheet) {272SheetContent()273// Available detents274.presentationDetents([275.medium,276.large,277.height(200),278.fraction(0.75)279])280// Selected detent binding281.presentationDetents([.medium, .large], selection: $selectedDetent)282// Drag indicator visibility283.presentationDragIndicator(.visible)284// Corner radius285.presentationCornerRadius(24)286// Background interaction287.presentationBackgroundInteraction(.enabled(upThrough: .medium))288// Prevent interactive dismiss289.interactiveDismissDisabled(hasUnsavedChanges)290}291}292}293```294295## Tab Navigation296297### Basic TabView (iOS 18+)298299```swift300struct MainTabView: View {301@State private var selectedTab = 0302303var body: some View {304TabView(selection: $selectedTab) {305Tab("Home", systemImage: "house", value: 0) {306HomeView()307}308309Tab("Search", systemImage: "magnifyingglass", value: 1) {310SearchView()311}312313Tab("Profile", systemImage: "person", value: 2) {314ProfileView()315}316.badge(unreadCount)317}318}319}320```321322### Tab with Custom Badge (iOS 18+)323324```swift325struct BadgedTabView: View {326@State private var selectedTab: AppTab = .home327@State private var cartCount = 3328329enum AppTab: String, CaseIterable {330case home, search, cart, profile331332var icon: String {333switch self {334case .home: return "house"335case .search: return "magnifyingglass"336case .cart: return "cart"337case .profile: return "person"338}339}340}341342var body: some View {343TabView(selection: $selectedTab) {344ForEach(AppTab.allCases, id: \.self) { tab in345Tab(tab.rawValue.capitalized, systemImage: tab.icon, value: tab) {346NavigationStack {347contentView(for: tab)348}349}350.badge(tab == .cart ? cartCount : 0)351}352}353}354}355```356357## Deep Linking358359### URL-Based Navigation360361```swift362struct DeepLinkableApp: App {363@StateObject private var router = NavigationRouter()364365var body: some Scene {366WindowGroup {367ContentView()368.environmentObject(router)369.onOpenURL { url in370router.handle(url: url)371}372}373}374}375376class NavigationRouter: ObservableObject {377@Published var path = NavigationPath()378@Published var selectedTab: Tab = .home379380func handle(url: URL) {381guard url.scheme == "myapp" else { return }382383switch url.host {384case "item":385if let id = Int(url.lastPathComponent) {386selectedTab = .home387path = NavigationPath()388path.append(Destination.itemDetail(id: id))389}390case "settings":391selectedTab = .profile392path = NavigationPath()393path.append(Destination.settings)394default:395break396}397}398}399```400401### Universal Links402403```swift404struct UniversalLinkHandler: View {405@EnvironmentObject private var router: NavigationRouter406407var body: some View {408ContentView()409.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in410guard let url = activity.webpageURL else { return }411handleUniversalLink(url)412}413}414415private func handleUniversalLink(_ url: URL) {416// Parse URL path and navigate accordingly417let pathComponents = url.pathComponents418419if pathComponents.contains("product"),420let idString = pathComponents.last,421let id = Int(idString) {422router.navigate(to: .product(id: id))423}424}425}426```427428## Navigation Coordinator Pattern429430```swift431@MainActor432class NavigationCoordinator: ObservableObject {433@Published var path = NavigationPath()434@Published var sheet: Sheet?435@Published var fullScreenCover: FullScreenCover?436437enum Sheet: Identifiable {438case settings439case newItem440case editItem(Item)441442var id: String {443switch self {444case .settings: return "settings"445case .newItem: return "newItem"446case .editItem(let item): return "editItem-\(item.id)"447}448}449}450451enum FullScreenCover: Identifiable {452case onboarding453case camera454455var id: String {456switch self {457case .onboarding: return "onboarding"458case .camera: return "camera"459}460}461}462463func push(_ destination: Destination) {464path.append(destination)465}466467func pop() {468guard !path.isEmpty else { return }469path.removeLast()470}471472func popToRoot() {473path = NavigationPath()474}475476func present(_ sheet: Sheet) {477self.sheet = sheet478}479480func presentFullScreen(_ cover: FullScreenCover) {481self.fullScreenCover = cover482}483484func dismiss() {485if fullScreenCover != nil {486fullScreenCover = nil487} else if sheet != nil {488sheet = nil489}490}491}492```493494## Navigation Transitions (iOS 18+)495496### Custom Navigation Transitions497498```swift499struct CustomTransitionView: View {500@Namespace private var namespace501502var body: some View {503NavigationStack {504List(items) { item in505NavigationLink(value: item) {506ItemRow(item: item)507.matchedTransitionSource(id: item.id, in: namespace)508}509}510.navigationDestination(for: Item.self) { item in511ItemDetailView(item: item)512.navigationTransition(.zoom(sourceID: item.id, in: namespace))513}514}515}516}517```518519### Hero Transitions520521```swift522struct HeroTransitionView: View {523@Namespace private var animation524@State private var selectedItem: Item?525526var body: some View {527ZStack {528ScrollView {529LazyVGrid(columns: columns) {530ForEach(items) { item in531if selectedItem?.id != item.id {532ItemCard(item: item)533.matchedGeometryEffect(id: item.id, in: animation)534.onTapGesture {535withAnimation(.spring(response: 0.3)) {536selectedItem = item537}538}539}540}541}542}543544if let item = selectedItem {545ItemDetailView(item: item)546.matchedGeometryEffect(id: item.id, in: animation)547.onTapGesture {548withAnimation(.spring(response: 0.3)) {549selectedItem = nil550}551}552}553}554}555}556```557