Sheet-Based Detail Views

Sheets are the standard way to show detail views in SwiftUI. Here's how to present them from grid or list selections.
Follow along with the code: iOS-Practice on GitHub
Item-Based Sheet Presentation
@State private var selectedPhoto: Photo?
.sheet(item: $selectedPhoto) { photo in
PhotoDetailSheet(photo: photo)
}
When selectedPhoto changes from nil to a value, the sheet appears. When dismissed, it's automatically set back to nil.
The Detail Sheet
struct PhotoDetailSheet: View {
let photo: Photo
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
VStack(spacing: 20) {
RoundedRectangle(cornerRadius: 16)
.fill(Color(named: photo.thumbnailColor))
.aspectRatio(4/3, contentMode: .fit)
.padding()
Text(photo.title)
.font(.title2)
.fontWeight(.bold)
Label(photo.category, systemImage: "folder")
.foregroundColor(.secondary)
Spacer()
}
.navigationTitle("Photo Detail")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
}
Environment Dismiss
@Environment(\.dismiss) private var dismiss
Button("Done") {
dismiss()
}
The dismiss action closes the sheet and sets the binding to nil.
Sheet vs Navigation
Use sheets for:
- Detail views that don't lead deeper
- Modal actions (create, edit)
- Settings/preferences
Use NavigationLink for:
- Drill-down hierarchies
- Master-detail flows
- Progressive disclosure
Connecting to Grid
ForEach(filteredPhotos) { photo in
PhotoGridCell(photo: photo)
.onTapGesture {
selectedPhoto = photo
}
}
Tapping a cell sets selectedPhoto, which triggers the sheet.
Sheet Sizes
Control presentation size on iPad:
.sheet(item: $selectedPhoto) { photo in
PhotoDetailSheet(photo: photo)
.presentationDetents([.medium, .large])
}
Boolean-Based Sheets
For simple sheets without data:
@State private var showSettings = false
Button("Settings") {
showSettings = true
}
.sheet(isPresented: $showSettings) {
SettingsView()
}
Preventing Dismissal
For forms that need completion:
.interactiveDismissDisabled(hasUnsavedChanges)
Full-Screen Alternative
.fullScreenCover(item: $selectedPhoto) { photo in
PhotoDetailView(photo: photo)
}
Full-screen covers don't have the drag-to-dismiss gesture.
Interview Tip
.sheet(item:) is preferred over .sheet(isPresented:) when you need to pass data. It's cleaner and avoids force-unwrapping optionals.