Skip to content
beginner Phase 4 · Navigation & Data Flow

TabView & Sheet Navigation

Implement tab-based navigation, modal sheets, and full-screen covers.

40m
2 problems
Topic Progress 0%

TabView Basics

What is TabView?

TabView is SwiftUI's container for building tab-based interfaces, similar to UITabBarController. It displays multiple child views as tabs at the bottom (or top) of the screen.

Each tab has an icon and label, and users switch between tabs by tapping the tab bar.

Creating a TabView

import SwiftUI

struct ContentView: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "house")
                }
            SearchView()
                .tabItem {
                    Label("Search", systemImage: "magnifyingglass")
                }
            ProfileView()
                .tabItem {
                    Label("Profile", systemImage: "person")
                }
        }
    }
}

Each .tabItem defines the tab bar appearance. You can use SF Symbols or custom images.

Binding to Selected Tab

Use a @State binding to control which tab is selected:

struct ContentView: View {
    @State private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "house")
                }
                .tag(0)
            SearchView()
                .tabItem {
                    Label("Search", systemImage: "magnifyingglass")
                }
                .tag(1)
            ProfileView()
                .tabItem {
                    Label("Profile", systemImage: "person")
                }
                .tag(2)
        }
    }
}

You can programmatically switch tabs by changing selectedTab.

Tab Badges

Add badge indicators to tabs:

NotificationView()
    .tabItem {
        Label("Notifications", systemImage: "bell")
    }
    .badge(5)

The badge displays a number or dot indicating unread items.

TabView with NavigationStack

Each tab typically wraps its content in a NavigationStack:

TabView(selection: $selectedTab) {
    NavigationStack {
        HomeView()
            .navigationTitle("Home")
    }
    .tabItem {
        Label("Home", systemImage: "house")
    }
    .tag(0)
}

This gives each tab its own independent navigation stack. Pushing a detail view in the Home tab won't affect the Search tab.

Hiding the Tab Bar

You can hide the tab bar in specific views:

DetailWebView()
    .toolbar(.hidden, for: .tabBar)

This is useful when navigating deep into a tab's hierarchy where the tab bar would be distracting.

Full-Screen Covers

Full-Screen Covers vs Sheets

Full-screen covers fill the entire screen, unlike sheets which leave part of the presenting view visible. They're used for immersive experiences like video playback, camera views, or authentication flows.

.fullScreenCover(isPresented: $isShowingCamera) {
    CameraView()
}

When to Use Full-Screen Covers

Use full-screen covers when:

  • The content needs the full screen (camera, video, games)
  • You don't want the sheet drag-to-dismiss behavior
  • The presented view should be opaque (no transparency)
  • You're presenting login/auth flows

Full-Screen Cover Dismissal

Unlike sheets, full-screen covers don't support swipe-to-dismiss. You must provide explicit dismissal:

struct CameraView: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        ZStack {
            CameraPreview()
            VStack {
                HStack {
                    Button("Cancel") { dismiss() }
                    Spacer()
                    Button("Capture") { capturePhoto() }
                }
                .padding()
            }
        }
    }
}

Combining Sheets and Full-Screen Covers

You can chain presentations — a sheet can present a full-screen cover:

.sheet(isPresented: $showProfile) {
    ProfileView()
        .fullScreenCover(isPresented: $showCamera) {
            CameraView()
        }
}

Custom Modifiers for Presentation

Create reusable presentation modifiers:

extension View {
    func editSheet(item: Binding<Item?>) -> some View {
        sheet(item: item) { item in
            EditView(item: item)
        }
    }
}

// Usage
MyView()
    .editSheet(item: $selectedItem)

This keeps your views clean and your presentation logic centralized.

Quiz

1. What modifier adds a label and icon to a TabView tab?

Question 1 options

2. How do you control which tab is selected in a TabView?

Question 2 options

3. What presentation detent shows a sheet at approximately half the screen?

Question 3 options

4. How do you prevent a user from swiping to dismiss a sheet?

Question 4 options

Flashcards

Question

What is the difference between .sheet and .fullScreenCover?

Answer

.sheet slides up from the bottom (partial screen, swipe dismiss). .fullScreenCover fills the entire screen (no swipe dismiss).

Question

How do you add a badge to a TabView tab?

Answer

Use .badge(count) on the view inside .tabItem.

Question

What are presentation detents?

Answer

Detents control how much of the screen a sheet covers: .medium, .large, or .fraction().

Question

How do you dismiss a sheet programmatically?

Answer

Use @Environment(\.dismiss) and call dismiss() to close the sheet.

Revision Notes

Key Takeaways

  • 1. TabView creates tab-based interfaces with .tabItem labels and .tag identifiers
  • 2. Use selection binding to programmatically control the active tab
  • 3. Sheets slide up partially and support swipe-to-dismiss; full-screen covers fill the screen
  • 4. Presentation detents (.medium, .large) control sheet height
  • 5. Each tab should have its own NavigationStack for independent navigation

Interview Tips

  • Know how to programmatically switch tabs using a selection binding
  • Explain when to use .sheet vs .fullScreenCover
  • Describe presentation detents and when to use each
  • Discuss how to pass data to and dismiss modal presentations

Cheat Sheet

TabView: Bottom tab bar with .tabItem and .tag.

Binding: selection: $selectedTab controls active tab.

Badge: .badge(count) on tab items.

Sheet: .sheet(isPresented:). Detents: .medium, .large, .fraction().

Full-Screen Cover: .fullScreenCover(isPresented:). Fills entire screen, no swipe dismiss.

Dismissal: @Environment(.dismiss) for both sheets and full-screen covers.

Tab Navigation: Wrap each tab in NavigationStack for independent nav stacks.