An example of how to build a reusable action sheet in SwiftUI
SwiftUI Action Sheet Example
An example of how to build a reusable action sheet in SwiftUI.
Usage
It’s as simple as calling .actionSheet
on any SwiftUI View
.
It’s very similar to using .fullscreenCover
or .sheet
if you’ve used those before.
struct ContentView: View {
// MARK: - Properties
@State private var isPresented: Bool = false
// MARK: - Content
private var button: some View {
Button(
action: {
isPresented = true
},
label: {
Text("Show action sheet").padding()
}
)
}
var body: some View {
button
.actionSheet(isPresented: $isPresented) { // Call `.actionSheet`
VStack {
Text("I'm an action sheet")
.padding()
Image(systemName: "questionmark")
.fixedSize()
.frame(height: 100)
Button("Dismiss") {
isPresented = false
}
.padding(48.0)
}
}
}
}
How does it work?
Check ActionSheet.swift
for the source code, although here is a brief rundown of the core ideas below:
1. First, we use a ViewModifier
to be able to wrap Views
The .actionSheet
function that you can call on any SwiftUI View
is built using a ViewModifier
.
It is defined like so:
extension View {
// Define the action sheet function that can be called on any `View`
func actionSheet(...) -> some View {
modifier(ActionSheetModifier(...)
}
}
struct ActionSheetModifier: ViewModifier {
// Our actual view modifier
// The `content` argument is the `View` that the `actionSheet` function is being called on
// We can manipulate the `View` / `content` however we want!
func body(content: Content) -> some View {
content
}
}
At a high-level, our ViewModifier
essentially wraps the content
in a ZStack
with our ActionSheet
:
struct ActionSheetModifier: ViewModifier {
@Binding var actionSheetIsPresented: Bool
func body(content: Content) -> some View {
ZStack {
content
if actionSheetIsPresented {
ActionSheet()
} else {
EmptyView()
}
}
}
}
The code above is simplified, but as you can see, ViewModifiers
are super powerful because we can wrap/encapsulate any view with another, simply by calling a function on a View
.
2. The second idea, is we allow our view modifier to take a @ViewBuilder
Without getting into the weeds of it, the @ViewBuilder
decorator allows our functions to take SwiftUI closures that return some View
as an argument.
For example, if you see the actionSheet
function below, there is a closure where you can build a SwiftUI View
, which will be used to populate the Action Sheet.
.actionSheet(isPresented: $isPresented) {
VStack {
Text("I'm an action sheet!")
}
}
This is a super cool feature of SwiftUI.
The trick to working with @ViewBuilder
I’ve found, is to use Generics. In otherwords, when writing your functions and classes that use @ViewBuilder
, add a generic constraint: <V: View>
:
// 1. We return a generic `View`, `V` in our `@ViewBuilder` closure
func actionSheet<V: View>(isPresented: Binding<Bool>, @ViewBuilder content: () -> V)
// 2. We use a generic `View`, `V` in our `ViewModifier` struct
struct ActionSheetModifier<V: View>: ViewModifier {
@ViewBuilder var actionSheetContent: V
func body(content: Content) -> some View {
// ...
ActionSheet {
actionSheetContent
}
}
}
// 3. The ActionSheet SwiftUI `View` itself has a generic constraint too that allows us to use `@ViewBuilder` to take a SwiftUI `View` as an input argument.
struct ActionSheet<Content: View>: View {
// ...
@ViewBuilder let content: Content
// ...
}
3. Using DragGesture()
and Binding
The last idea here, is that we used DragGesture()
to handle panning the Action Sheet vertically.
Using @Binding
also provided us with a bi-directional data flow, allowing our Action Sheet struct to dismiss itself and set isPresented = false
, and also allow the View
that calls the .actionSheet
function to toggle isPresented
as well.
There are many great tutorials on gestures and bindings in Swift, so I’d recommend checking those out for more clarification.
Conclusion
I hope this example helps! Feel free to check out my website at https://www.josharnold.me.