From apple-dev
SwiftUI presentation patterns including sheets, fullScreenCover, popover, inspector, alert, confirmationDialog, and presentation customization. Use when presenting modal or overlay content.
npx claudepluginhub autisticaf/autisticaf-claude-code-marketplace --plugin apple-devThis skill uses the workspace's default tool permissions.
> **First step:** Tell the user: "swiftui-presentations skill loaded."
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
First step: Tell the user: "swiftui-presentations skill loaded."
Modal and overlay content patterns for SwiftUI apps.
Use this skill when the user:
What kind of content are you presenting?
|
+- Contextual info or form (non-blocking)
| +- iPhone -> .sheet (auto adapts to full height)
| +- iPad/Mac, anchored to element -> .popover
| +- iPad/Mac, side panel -> .inspector
|
+- Immersive flow (onboarding, login, full editor)
| +- .fullScreenCover
|
+- Destructive or irreversible action confirmation
| +- .confirmationDialog
|
+- Short message with simple actions (OK, Cancel)
| +- .alert
|
+- Navigating to a new screen in a stack
| +- NavigationLink (not a presentation)
| API | Minimum Version | Notes |
|---|---|---|
.sheet(isPresented:) | iOS 14 | Boolean-driven sheet |
.sheet(item:) | iOS 14 | Item-driven sheet |
.fullScreenCover(isPresented:) | iOS 14 | Full-screen modal |
.fullScreenCover(item:) | iOS 14 | Item-driven full-screen modal |
.popover(isPresented:) | iOS 14 | Popover (iPad/Mac; sheet on iPhone) |
.alert(title:isPresented:) | iOS 15 | Alert with actions and message |
.confirmationDialog(title:isPresented:) | iOS 15 | Action sheet replacement |
.presentationDetents() | iOS 16 | Half-sheet and custom heights |
.presentationDragIndicator() | iOS 16 | Show/hide grab handle |
.presentationCornerRadius() | iOS 16.4 | Custom corner radius |
.presentationContentInteraction() | iOS 16.4 | Scroll vs. resize priority |
.presentationBackgroundInteraction() | iOS 16.4 | Interact with content behind sheet |
.interactiveDismissDisabled() | iOS 15 | Prevent swipe-to-dismiss |
.inspector(isPresented:) | iOS 17 | Side panel inspector |
@State private var showSettings = false
Button("Settings") { showSettings = true }
.sheet(isPresented: $showSettings) {
SettingsView()
}
The sheet appears when the item is non-nil. The item must conform to Identifiable.
@State private var selectedItem: Item?
List(items) { item in
Button(item.name) { selectedItem = item }
}
.sheet(item: $selectedItem) { item in
ItemDetailView(item: item)
}
Use for immersive flows. No swipe-to-dismiss gesture; slides up from the bottom.
@State private var showOnboarding = true
MainView()
.fullScreenCover(isPresented: $showOnboarding) {
OnboardingFlow()
}
Floating panel on iPad/Mac; adapts to sheet on iPhone.
@State private var showFilter = false
Button("Filter") { showFilter = true }
.popover(isPresented: $showFilter,
attachmentAnchor: .point(.bottom),
arrowEdge: .top) {
FilterView()
.frame(minWidth: 300, minHeight: 400)
}
attachmentAnchor: Where on the source view the popover attaches (.point(.top), .rect(.bounds))arrowEdge: Which edge of the popover the arrow appears onSide panel on iPad/Mac; sheet on iPhone.
@State private var showInspector = false
EditorView()
.inspector(isPresented: $showInspector) {
InspectorContent()
.inspectorColumnWidth(min: 250, ideal: 300, max: 400)
}
Control the height stops for a sheet. Available from iOS 16+.
.sheet(isPresented: $showPanel) {
PanelView()
.presentationDetents([.medium, .large])
}
// Fixed height
.presentationDetents([.height(200)])
// Fraction of screen
.presentationDetents([.fraction(0.25), .large])
// Custom detent with context
struct MyCustomDetent: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
max(context.maxDetentValue * 0.3, 200)
}
}
// Usage
.presentationDetents([.custom(MyCustomDetent.self), .large])
@State private var selectedDetent: PresentationDetent = .medium
.sheet(isPresented: $showPanel) {
PanelView()
.presentationDetents([.medium, .large], selection: $selectedDetent)
}
Apply inside the presented view:
ScrollView { LongFormContent() }
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
.presentationCornerRadius(20)
.presentationContentInteraction(.scrolls)
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
| Modifier | Purpose |
|---|---|
.presentationDragIndicator(.visible) | Show/hide the grab handle |
.presentationCornerRadius(20) | Custom corner radius (iOS 16.4+) |
.presentationContentInteraction(.scrolls) | Prioritize scrolling over sheet resizing |
.presentationBackgroundInteraction(.enabled) | Allow interaction with content behind sheet |
@State private var showAlert = false
Button("Delete", role: .destructive) { showAlert = true }
.alert("Delete Item?", isPresented: $showAlert) {
Button("Delete", role: .destructive) { performDelete() }
Button("Cancel", role: .cancel) { }
} message: {
Text("This action cannot be undone.")
}
Use instead of alert when offering multiple actions. Renders as action sheet on iPhone, popover on iPad/Mac.
.confirmationDialog("Change Photo", isPresented: $showDialog, titleVisibility: .visible) {
Button("Take Photo") { takePhoto() }
Button("Choose from Library") { chooseFromLibrary() }
Button("Remove Photo", role: .destructive) { removePhoto() }
} message: {
Text("Select a new profile photo.")
}
struct DetailSheet: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
Form { /* ... */ }
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { dismiss() }
}
}
}
}
}
struct ChildView: View {
@Binding var isPresented: Bool
var body: some View {
Button("Close") { isPresented = false }
}
}
Block swipe-to-dismiss while allowing programmatic dismissal. Pass true to always block, or a condition.
@State private var hasUnsavedChanges = false
Form { /* ... */ }
.interactiveDismissDisabled(hasUnsavedChanges)
// ❌ Multiple sheets on the same view — only the last one wins
VStack {
Text("Hello")
}
.sheet(isPresented: $showA) { ViewA() }
.sheet(isPresented: $showB) { ViewB() }
// ✅ Attach each sheet to a different child view, or use item-based presentation
VStack {
Button("A") { showA = true }
.sheet(isPresented: $showA) { ViewA() }
Button("B") { showB = true }
.sheet(isPresented: $showB) { ViewB() }
}
// ❌ Setting item inside the sheet's onDismiss — causes re-presentation loop
.sheet(item: $selectedItem, onDismiss: { selectedItem = anotherItem }) {
DetailView(item: $0)
}
// ✅ Use onDismiss only for cleanup, not for triggering new presentations
.sheet(item: $selectedItem, onDismiss: { cleanUpResources() }) {
DetailView(item: $0)
}
// ❌ Forgetting Identifiable on the item type
struct Task { // No Identifiable conformance
let name: String
}
// .sheet(item:) won't compile
// ✅ Conform to Identifiable
struct Task: Identifiable {
let id = UUID()
let name: String
}
// ❌ Calling dismiss() on the parent instead of inside the sheet
struct Parent: View {
@Environment(\.dismiss) private var dismiss
@State private var showSheet = false
var body: some View {
Button("Show") { showSheet = true }
.sheet(isPresented: $showSheet) {
Button("Close") { dismiss() } // Dismisses Parent, not the sheet
}
}
}
// ✅ Access @Environment(\.dismiss) inside the presented view
struct SheetContent: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("Close") { dismiss() } // Correctly dismisses the sheet
}
}