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 @State property in the view that determines whether to show the sheet.
  • Code to set that property to true from the UI.
  • A .sheet modifier 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 @State property to store the sheet to show.
  • Add a .focusedSceneValue modifier if you want to show sheets from menus.
  • Set the sheet to show from the view’s UI.
  • Add a .sheet modifier 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)
}

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.