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:

@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:

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 @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.

Get the Swift Dev Journal Newsletter

Subscribe and get exclusive articles, a free guide on moving from tutorials to making your first app, notices of sales on books, and anything I decide to add in the future.

    We won't send you spam. Unsubscribe at any time.