Reducing the Number of .sheet Modifiers in Your SwiftUI Views
Showing sheets is something most SwiftUI apps do. The most common way to show sheets is to have the following items in a SwiftUI view:
- A
@Stateproperty in the view that determines whether to show the sheet. - Code to set that property to
truefrom the UI. - A
.sheetmodifier to show a view in a sheet.
@State private var showSheet = false
Button {
showSheet = true
} label: {
Label("Add Page", systemImage: "plus")
}
.sheet(isPresented: $showSheet) {
AddPageView()
}
This way of showing sheets works well if your app shows one or two sheets in a view. If your app shows more sheets in a view, you wind up with a @State property and a .sheet modifier for each sheet your app can show. Add enough sheets, and your code becomes a mess.
You can reduce the number of Boolean sheet showing properties and .sheet modifiers by doing the following:
- Create an enum for the sheets you want to show.
- Create a sheet view to show the correct sheet based on the enum you create.
- Create a focused value to show sheets from menu items.
Creating an enum for the sheets
Create an enum with one case for each sheet your app shows. The enum should conform to the Identifiable and Hashable protocols.
enum Sheet: Identifiable, Hashable {
case addBookmark
case addChange
case addRemote
case editChangeDescription
case push
case clone
var id: Self { self }
}
Creating a sheet view
Start by creating a SwiftUI view. Add a constant that lets the sheet view know what sheet to know. The type for the constant should be the enum you created. In the body property, write a switch statement on the enum to show the correct sheet.
struct SheetView: View {
let sheet: Sheet
var body: some View {
switch sheet {
case .addBookmark:
AddBookmarkView()
case .addChange:
AddChangeView()
case .addRemote:
AddRemoteView()
case .editChangeDescription:
EditDescriptionView()
case .push:
PushView()
case .clone:
CloneView()
}
}
}
Creating a focused value
If you want to show sheets from menu items, you must create a focused value that stores the sheet to show. Add an extension to the FocusedValues struct to create the focused value property. The type of the focused value should be an optional binding to the enum you created.
extension FocusedValues {
@Entry var sheetToShow: Binding<Sheet>?
}
The @Entry macro supports iOS 18+ and macOS 15+.
In the views for your menu items that show sheets, add a @FocusedValue property so you can set the sheet to show.
@FocusedValue(\.sheetToShow) private var sheet
In your menu’s UI code, set the sheet property’s wrapped value to the enum for the sheet you want to show.
Button(action: {
sheet?.wrappedValue = .clone
}, label: {
Text("Clone")
})
Showing a sheet
To show a sheet from a SwiftUI view, you must do the following:
- Add a
@Stateproperty to store the sheet to show. - Add a
.focusedSceneValuemodifier if you want to show sheets from menus. - Set the sheet to show from the view’s UI.
- Add a
.sheetmodifier to show the sheet.
Add @State property
Add a @State property to the SwiftUI view to store the enum for the sheet to show.
@State private var sheetToShow: Sheet? = nil
The type must be an optional value so you can pass the sheet enum to the .sheet modifier.
Add .focusedSceneValue modifier
If you are showing sheets from menus, add a .focusedSceneValue modifier to the view. The modifier takes two arguments. The name of the first argument must match the name of the variable you added as a FocusedValues extension. The second argument is a binding to the @State property you added to the view.
.focusedSceneValue(\.sheetToShow, $sheetToShow.safeBinding(defaultValue: .addChange))
The safeBinding function is an extension to convert an optional binding to a non-optional binding. The .focusedSceneValue modifier cannot accept a binding to an optional value so you must convert the sheetToShow value to a non-optional value.
extension Binding {
// Convert an optional binding to a non-optional one.
func safeBinding<T: Sendable>(defaultValue: T) -> Binding<T> where Value == Optional<T> {
Binding<T>.init {
self.wrappedValue ?? defaultValue
} set: { newValue in
self.wrappedValue = newValue
}
}
}
The code for the safeBinding function comes from the Stack Overflow question Converting optional Binding to non-optional Binding.
Set the sheet to show
Set the sheet to show from the view’s UI.
Button(action: {
sheetToShow = .push
}, label: {
Label("Push", systemImage:"square.and.arrow.up")
}).accessibilityLabel("Push Changes")
Add .sheet modifier
Finally add a .sheet modifier to the view and pass a binding to the @State property as the item argument. Show the sheet view inside the closure. The argument you pass to the closure contains the enum value that tells the sheet view what sheet to show.
.sheet(item: $sheetToShow) { sheet in
SheetView(sheet: sheet)
}
Related reading
Azam Sharp wrote the article Global Sheets Pattern in SwiftUI that covers how to show global sheets in SwiftUI apps. Azam’s article focuses more on iOS and using environment values while this article focuses more on Mac and focused values. His article also shows how to support older OS versions that don’t support the @Entry macro.