How to make a reusable ActionSheet in SwiftUI | SwiftUI Bootcamp #33
When users tap the "..." button on a social media post, they see different options depending on whether they own the post — Share and Report for others' posts, plus Delete for their own. This lesson shows you how to build exactly that pattern: a dynamic action sheet that adapts its buttons based on context, using the same enum-driven technique from the alerts lesson.
What You'll Learn
- How
.actionSheet(isPresented:content:)works and how it differs from.alert - How to build named
ActionSheet.Buttonvalues and assemble them dynamically - How to use an enum to select between different sets of actions based on context
Mental Model
An ActionSheet is like a context-sensitive right-click menu. The options that appear depend on what was right-clicked — a file shows "Open", "Copy", "Delete"; a folder shows different options. The menu itself is the same visual component; the content varies based on context.
In this lesson, the context is "is this my post or someone else's?" The enum ActionSheetOptions captures that context. The function getActionSheet() reads the context and assembles the appropriate set of buttons. The visual presentation mechanism (.actionSheet(isPresented:)) is always the same.
Detailed Explanation
.actionSheet(isPresented: $showActionSheet, content: getActionSheet) is structurally identical to .alert from lesson 32. The same patterns apply: a Boolean drives presentation, an enum identifies which sheet to show, and a function builds the content. SwiftUI resets the Boolean on dismissal automatically.
ActionSheet.Button is built with one of three class methods:
.default(Text("Label")) { action }— a standard tappable option.destructive(Text("Label")) { action }— rendered in red, for irreversible actions like Delete.cancel()— a system cancel button that's always placed at the bottom
You build named button constants first (let shareButton, let deleteButton, etc.) and then pass arrays of them into ActionSheet(title:message:buttons:). This is more readable than building the array inline, especially when buttons are shared across multiple switch cases.
The key design insight in this example: shareButton and reportButton appear in both the isMyPost and isOtherPost cases. deleteButton only appears in isMyPost. By defining buttons as constants at the top of the function, you can compose different button arrays per case without repeating the button definitions.
Unlike Alert, ActionSheet can contain many buttons — as many as the use case requires. But keep the list short in practice: iOS human interface guidelines recommend no more than 5-6 options before users find the sheet overwhelming.
Code Structure
ActionsheetBootcamp.swift simulates a social post card with a profile circle, username, and placeholder image. The "..." button sets the enum case to .isMyPost before toggling the sheet. getActionSheet() switches on the enum to return either a 3-button or 4-button ActionSheet. The commented-out code shows the simpler version with hardcoded buttons for reference.
Complete Code
ActionsheetBootcamp.swift
import SwiftUI
struct ActionsheetBootcamp: View {
@State var showActionSheet: Bool = false
@State var actionSheetOption: ActionSheetOptions = .isOtherPost // default to "viewing someone else's post"
enum ActionSheetOptions {
case isMyPost
case isOtherPost
}
var body: some View {
VStack {
HStack {
Circle()
.frame(width: 30, height: 30)
Text("@username")
Spacer()
Button(action: {
actionSheetOption = .isMyPost // set context: this is our post
showActionSheet.toggle() // then trigger the sheet
}, label: {
Image(systemName: "ellipsis") // "..." icon — standard iOS pattern for more options
})
.accentColor(.primary) // uses the system foreground color (black/white based on appearance)
}
.padding(.horizontal)
Rectangle()
.aspectRatio(1.0, contentMode: .fit) // square image placeholder
}
.actionSheet(isPresented: $showActionSheet, content: getActionSheet) // function reference, not a closure call
}
func getActionSheet() -> ActionSheet {
let shareButton: ActionSheet.Button = .default(Text("Share")) {
// add code to share post
}
let reportButton: ActionSheet.Button = .destructive(Text("Report")) {
// add code to report this post
}
let deleteButton: ActionSheet.Button = .destructive(Text("Delete")) {
// add code to delete this post
}
let cancelButton: ActionSheet.Button = .cancel()
let title = Text("What woud you like to do?")
switch actionSheetOption {
case .isOtherPost:
return ActionSheet( // 3 buttons: share, report, cancel — no delete
title: title,
message: nil,
buttons: [shareButton, reportButton, cancelButton])
case .isMyPost:
return ActionSheet( // 4 buttons: share, report, delete, cancel — delete is now included
title: title,
message: nil,
buttons: [shareButton, reportButton, deleteButton, cancelButton])
}
//return ActionSheet(title: Text("This is the title!"))
// let button1: ActionSheet.Button = .default(Text("DEFAULT"))
// let button2: ActionSheet.Button = .destructive(Text("DESTRUCTIVE"))
// let button3: ActionSheet.Button = .cancel()
//
// return ActionSheet(
// title: Text("This is the title!"),
// message: Text("This is the message."),
// buttons: [button1, button1, button1, button1, button1, button2, button3])
}
}
struct ActionsheetBootcamp_Previews: PreviewProvider {
static var previews: some View {
ActionsheetBootcamp()
}
}Code Walkthrough
@State var actionSheetOption: ActionSheetOptions = .isOtherPost— Defaults to viewing another user's post. In a real app, you'd set this based on whether the authenticated user matches the post's author.actionSheetOption = .isMyPostbeforeshowActionSheet.toggle()— The same ordering discipline as with alerts: set the context first, then trigger the presentation. If you toggle first, the content function might fire before the context is updated..actionSheet(isPresented: $showActionSheet, content: getActionSheet)— NotegetActionSheet(without parentheses). This passes the function reference, not its return value. SwiftUI calls this function when it's ready to present the sheet. This is equivalent to using a trailing closure but cleaner for named functions.Named button constants —
shareButton,reportButton,deleteButton, andcancelButtonare defined once. They're reused across switch cases. If you later change the share button's label or action, you update one place.case .isOtherPost:returns 3 buttons — No delete option. A non-owner has no business deleting someone else's content.case .isMyPost:returns 4 buttons including.destructive(Text("Delete"))— The destructive style renders "Delete" in red. iOS also ensures the cancel button appears at the very bottom, separated from the other buttons, regardless of where you put it in the array.
Common Mistakes
Mistake: Forgetting the .cancel() button
Action sheets without a cancel button leave users with no obvious way to dismiss the sheet except tapping outside it on iPad (where it functions as a popover). Always include .cancel() as the last button.
Mistake: Putting destructive actions in .default style.default renders in standard blue. For "Delete" or "Report", use .destructive to render in red. This visual cue is an iOS convention — users expect red to signal danger. Using the wrong style confuses users about the severity of the action.
Mistake: Updating actionSheetOption inside the sheet's button action
The sheet is being dismissed at the same time the action runs. Updating state during dismissal can cause race conditions in complex scenarios. Perform your side effects (network calls, data mutations) in the button actions, but avoid toggling the enum that controls which sheet to show.
Key Takeaways
.actionSheetfollows the same Boolean + enum + function pattern as.alert— learn one, and the other is immediately familiar.- Define
ActionSheet.Buttonas named constants at the top of the builder function; this makes it easy to compose different button sets per context without duplication. - Always include a
.cancel()button and use.destructivestyle for irreversible actions — these are iOS UI conventions that users depend on.
Last updated: June 27, 2026