Display pop-up Sheets and FullScreenCovers in SwiftUI | SwiftUI Bootcamp #28
Tapping a button to open a settings screen, a photo picker, or an onboarding flow is one of the most common patterns in iOS. SwiftUI provides sheet and fullScreenCover as first-class modifiers that tie presentation to a Boolean state — no present(_:animated:), no view controller dance.
What You'll Learn
- How to present a secondary screen using
.sheet(isPresented:content:)and the difference from.fullScreenCover - How to dismiss a presented sheet programmatically using
@Environment(\.presentationMode) - Why sheet presentation in SwiftUI is driven by state, and what "do not add conditional logic" in the content closure means
Mental Model
Think of .sheet like a trapdoor under a theater stage. The main stage (SheetsBootcamp) is always present. When the Boolean flips to true, the trapdoor opens and a new performer (SecondScreen) rises up on a platform — partially covering the main stage. The main stage is still there behind it; you can see its edge. When the new screen dismisses (trapdoor closes), it drops back down and the main stage is fully visible again.
.fullScreenCover is the same idea but with a full wall instead of a trapdoor — the new screen completely replaces what you see, with no peek of the original screen below.
Detailed Explanation
.sheet(isPresented: $showSheet, content: { SecondScreen() }) is a modifier you attach to any view. When showSheet becomes true, SwiftUI presents the content as a modal sheet. When showSheet returns to false (either by swiping down or by explicit dismissal code), the sheet disappears.
The content closure runs once when the sheet is about to be presented. The comment // DO NOT ADD CONDITIONAL LOGIC in the code refers to a real gotcha: if you write if someCondition { ScreenA() } else { ScreenB() } inside the content closure, SwiftUI evaluates the condition at presentation time and cannot update the content while the sheet is open. Keep the closure simple: return exactly one view.
@Environment(\.presentationMode) gives the child view access to its own presentation context. Calling presentationMode.wrappedValue.dismiss() sends a signal back to the parent to set showSheet = false. This is the SwiftUI 1.0/2.0 API. In iOS 15+, @Environment(\.dismiss) is the cleaner alternative: dismiss() directly.
The difference between sheet and fullScreenCover: a sheet slides up from the bottom and shows the underlying view peeking above it (the user can swipe it down). A fullScreenCover covers the entire screen with no underlying view visible and cannot be swiped down by default. Use sheet for contextual overlays; use fullScreenCover for onboarding, login, or any flow where you don't want the user to casually swipe back.
Code Structure
SheetsBootcamp.swift contains two structs: SheetsBootcamp (the presenting view that owns showSheet) and SecondScreen (the presented view that uses @Environment to dismiss itself). The commented-out .fullScreenCover line shows how trivially easy it is to switch presentation styles.
Complete Code
SheetsBootcamp.swift
import SwiftUI
struct SheetsBootcamp: View {
@State var showSheet: Bool = false // controls whether SecondScreen is presented
var body: some View {
ZStack {
Color.green
.edgesIgnoringSafeArea(.all)
Button(action: {
showSheet.toggle() // sets showSheet to true; SwiftUI presents the sheet
}, label: {
Text("Button")
.foregroundColor(.green)
.font(.headline)
.padding(20)
.background(Color.white.cornerRadius(10))
})
// .fullScreenCover(isPresented: $showSheet, content: { // swap this in to get a full-screen version
// SecondScreen()
// })
.sheet(isPresented: $showSheet, content: {
// DO NOT ADD CONDITIONAL LOGIC — the closure should return exactly one view
SecondScreen()
})
}
}
}
struct SecondScreen: View {
@Environment(\.presentationMode) var presentationMode // access to this view's presentation context
var body: some View {
ZStack(alignment: .topLeading) {
Color.red
.edgesIgnoringSafeArea(.all)
Button(action: {
presentationMode.wrappedValue.dismiss() // tells SwiftUI to set showSheet = false in the parent
}, label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.largeTitle)
.padding(20)
})
}
}
}
struct SheetsBootcamp_Previews: PreviewProvider {
static var previews: some View {
SheetsBootcamp()
//SecondScreen()
}
}Code Walkthrough
@State var showSheet: Bool = false— The entire presentation lifecycle is controlled by this one Boolean. SwiftUI observes it; when it becomestrue, the sheet appears. When it returns tofalse, the sheet dismisses..sheet(isPresented: $showSheet, content: { SecondScreen() })— This modifier is attached to theButton. The$showSheetpasses a binding so SwiftUI can set it back tofalsewhen the user swipes the sheet down. The content closure returnsSecondScreen().// DO NOT ADD CONDITIONAL LOGIC— This comment exists because a common mistake is writingif condition { ViewA() } else { ViewB() }inside the content closure. The content is captured once at presentation time, so conditional switching inside the closure doesn't work reliably.@Environment(\.presentationMode) var presentationMode— This is an environment value injected automatically intoSecondScreenbecause it's being presented. It provides a handle to calldismiss().presentationMode.wrappedValue.dismiss()— The.wrappedValueis needed becausepresentationModeis wrapped in aBinding. In iOS 15+, you'd use@Environment(\.dismiss) var dismissand calldismiss()directly — no.wrappedValueneeded.The commented
.fullScreenCoverline — To switch from sheet to full screen, you uncomment this and comment out the.sheetbelow it. TheSecondScreencontent stays identical — the only change is the presentation style. This illustrates how SwiftUI separates presentation mechanism from content.
Common Mistakes
Mistake: Forgetting to pass $showSheet (using showSheet without $)
Without the $, SwiftUI gets the Boolean value at the moment of the modifier call, not a reference to it. The sheet will never be able to dismiss itself by setting the value back to false. Always use $ for isPresented.
Mistake: Adding complex conditional logic inside the sheet content closure
The content closure is evaluated once when the sheet is presented, not reactively. To show different content based on state, set up the state before presenting the sheet (e.g., set an enum value indicating which screen to show, then present).
Mistake: Using fullScreenCover where sheet is more appropriatefullScreenCover hides the context the user was just in. For contextual actions (like editing a single item, sharing, or picking from a list), a sheet that shows the underlying context is usually the better UX. Reserve fullScreenCover for flows that completely replace the current context.
Key Takeaways
- SwiftUI sheet and fullScreenCover presentation is state-driven — a Boolean controls whether the screen is shown or hidden.
@Environment(\.presentationMode)gives presented views the ability to dismiss themselves without needing a direct reference to the parent's state.sheetshows the underlying view partially;fullScreenCoverreplaces the entire screen — choose based on whether context is useful to the user.
Last updated: June 27, 2026