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.
Modal Sheets
Presenting Sheets
Sheets are modal views that slide up from the bottom of the screen. They're used for secondary tasks like composing an email, editing a profile, or selecting options.
struct ContentView: View {
@State private var isShowingSheet = false
var body: some View {
Button("Show Settings") {
isShowingSheet = true
}
.sheet(isPresented: $isShowingSheet) {
SettingsView()
}
}
}
Detents
Sheets support detents, which control how much of the screen the sheet covers:
.sheet(isPresented: $isShowingSheet) {
SettingsView()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
Available detents:
.medium— approximately half the screen.large— full screen.fraction(0.75)— custom fraction
Passing Data to Sheets
Sheets can receive data via initialization:
.sheet(isPresented: $isEditing) {
EditView(item: selectedItem)
}
Or use .sheet(item:) for presenting data-dependent sheets:
.sheet(item: $selectedItem) { item in
EditView(item: item)
}
The item binding automatically dismisses when set to nil.
Sheet Dismissal
Sheets dismiss when the user swipes down, or programmatically:
struct EditView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
Form {
// Form fields
}
.navigationTitle("Edit")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
save()
dismiss()
}
}
}
}
}
}
Interactive Dismissal
Control whether users can swipe to dismiss:
.sheet(isPresented: $isShowing) {
ImportantView()
.interactiveDismissDisabled()
}
This prevents accidental dismissal of important content.
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?
2. How do you control which tab is selected in a TabView?
3. What presentation detent shows a sheet at approximately half the screen?
4. How do you prevent a user from swiping to dismiss a sheet?
Flashcards
Question
What is the difference between .sheet and .fullScreenCover?
Click to reveal answer
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?
Click to reveal answer
Answer
Use .badge(count) on the view inside .tabItem.
Question
What are presentation detents?
Click to reveal answer
Answer
Detents control how much of the screen a sheet covers: .medium, .large, or .fraction().
Question
How do you dismiss a sheet programmatically?
Click to reveal answer
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.