Jul 1, 2026

Enum Sheets

Enum Swift SwiftUI
Enum Sheets

Using Enums That Conform to View for Cleaner Sheet Presentations in SwiftUI

SwiftUI makes presenting sheets incredibly easy, but as an application grows, managing multiple sheets can become messy. It's common to see several Boolean properties dedicated to controlling different presentations:

@State private var showingSettings = false@State private var showingProfile = false@State private var showingHelp = false

This approach works, but it doesn't scale well. Every new sheet requires another state property, additional presentation logic, and more maintenance.

In this blog post, I'll show you a technique I use to simplify sheet presentations by creating an enum that conforms to both View and Identifiable. The result is cleaner code, fewer state properties, and a more scalable architecture.

The Traditional Approach

Consider a view with three buttons that present three different sheets.

struct TradionionalView: View {    @State private var showingSettings = false    @State private var showingProfile = false    @State private var showingHelp = false    var body: some View {        NavigationStack {            VStack {                Button("Settings") {                    showingSettings = true                }                Button("Profile") {                    showingProfile = true                }                Button("Help") {                    showingHelp = true                }            }            .buttonStyle(.bordered)            .navigationTitle("Enum Sheets")            .sheet(isPresented: $showingSettings) {                SettingsView()            }            .sheet(isPresented: $showingProfile) {                ProfileView()            }            .sheet(isPresented: $showingHelp) {                HelpView()            }        }    }}

iPhone 17 Pro - 2026-06-02 at 11.09.08

Although functional, this solution has several drawbacks:

  • Multiple Boolean state properties
  • Multiple sheet modifiers
  • More code to maintain
  • Greater chance of presentation conflicts

A Better Solution

Instead of tracking multiple Boolean values, we can track a single enum value representing the sheet that should be presented.

First, create an enum that conforms to Identifiable and View.

enum ActiveSheet: Identifiable, View {    case settings    case profile    case help    var id: Self { self }    var body: some View {        switch self {        case .settings:            SettingsView()        case .profile:            ProfileView()        case .help:            HelpView()        }    }}

This enum serves two purposes:

  1. It identifies which sheet should be presented.
  2. It knows how to build the view associated with each case.

Presenting the Sheet

Now we only need a single optional state property.

@State private var activeSheet: ActiveSheet?

The main view becomes much simpler.

struct EnumSheetView: View {    @State private var activeSheet: ActiveSheet?    var body: some View {        NavigationStack {            VStack {                Button("Settings") {                    activeSheet = .settings                }                Button("Profile") {                    activeSheet = .profile                }                                Button("Help") {                    activeSheet = .help                }            }            .buttonStyle(.bordered)            .navigationTitle("Enum Sheets")            .sheet(item: $activeSheet) { sheet in               sheet            }        }    }}

That's it.

The sheet modifier receives the enum instance and simply presents it because the enum itself conforms to View.

Why This Works

The .sheet(item:) modifier requires an optional value that conforms to Identifiable.

By making the enum conform to Identifiable, SwiftUI can determine when a new sheet should be presented.

var id: Self { self }

Because each enum case is unique, using self as the identifier works perfectly.

At the same time, conforming to View allows each case to generate its own destination view.

var body: some View {    switch self {    case .settings:        SettingsView()    case .profile:        ProfileView()    case .help:        HelpView()    }}

Because the enum conforms to View, there is no need to add @ViewBuilder to the body property here. The switch branches each return a view, and SwiftUI can infer the result.

The enum becomes both the navigation state and the destination view.

Supporting Associated Values

This pattern becomes even more powerful when working with associated values.

Consider this, where I have a new enum that is similar to ActiveSheet, except that the help case now has an associated value that is a String.

enum ActiveSheet2: Identifiable, View {    case settings    case profile    case help(String)    var id: String {        switch self {        case .help:            return "help"        default:            return String(describing: self)        }    }    var body: some View {        switch self {        case .settings:            SettingsView()        case .profile:            ProfileView()        case .help(let helpString):            HelpView(helpString: helpString)        }    }}

Note. Because the enum has at least one case with an associated value, we needed to change the id property to String and provide a specific value for the help case. For the other two cases that do not have associated values, the id property can be String(describing:

For the enum body, we can extract the associated value from the case and pass that in to the HelpView that can also accept a String parameter

case .help(let helpString):    HelpView(helpString: helpString)}

Presenting a specific user becomes straightforward.

Button("Help") {    activeSheet = .help("This is the associated help string for this second view")}

The enum now carries both the destination type and any data required by the destination.

The Final Refinement

Once the enum conforms to View, the sheet presentation code can be simplified even further.

Many developers write:

.sheet(item: $activeSheet) { sheet in    sheet}

While perfectly valid, the closure is simply returning the value it receives.

Since ActiveSheet already conforms to View, we can shorten this to:

.sheet(item: $activeSheet) { $0 }

This works because the closure parameter is an instance of ActiveSheet, and ActiveSheet is itself a view.

At this point, the enum serves three distinct roles:

  • Navigation state
  • Sheet identifier
  • Sheet content

The entire presentation logic is reduced to a single line of code.

.sheet(item: $activeSheet) { $0 }

This is one of the most compelling reasons to adopt this pattern. The presentation code becomes almost trivial while remaining fully type-safe.

The Evolution of the Pattern

It's interesting to see how this approach evolves as your SwiftUI knowledge grows.

Step 1: Multiple Boolean Properties

@State private var showingSettings = false@State private var showingProfile = false@State private var showingHelp = false

Step 2: A Single Enum State

@State private var activeSheet: ActiveSheet?

Step 3: The Enum Becomes the Destination

enum ActiveSheet: Identifiable, View {    case settings    case profile    case help    var id: Self { self }    var body: some View {        switch self {        case .settings:            SettingsView()        case .profile:            ProfileView()        case .help:            HelpView()        }    }}

Step 4: Presentation Logic Becomes One Line

.sheet(item: $activeSheet) { $0 }

At this stage, the enum completely encapsulates the sheet presentation logic. All the view needs to do is assign the appropriate case.

Advantages of This Pattern

Single Source of Truth

Only one state property controls all sheet presentations.

@State private var activeSheet: ActiveSheet?

Easy to Extend

Adding another sheet only requires a new enum case.

case about

And one additional switch branch.

case .about:    AboutView()

Better Organization

All sheet destinations live in one place instead of being scattered throughout the view hierarchy.

Type Safety

The compiler ensures every case is handled in the switch statement.

Less Boilerplate

No more collection of Boolean flags cluttering your view.

When Should You Use This?

This pattern works especially well when:

  • A view can present multiple sheets
  • Sheet destinations are known in advance
  • You want centralized presentation logic
  • You want to reduce state management complexity

For simple views with a single sheet, the traditional Boolean approach is perfectly acceptable. However, once you have three or more destinations, this enum-based approach often becomes easier to maintain.

Complete Example

enum ActiveSheet2: Identifiable, View {    case settings    case profile    case help(String)    var id: String {        switch self {        case .help:            return "help"        default:            return String(describing: self)        }    }    var body: some View {        switch self {        case .settings:            SettingsView()        case .profile:            ProfileView()        case .help(let helpString):            HelpView(helpString: helpString)        }    }}struct EnumAssociatedValueView: View {    @State private var activeSheet: ActiveSheet2?    var body: some View {        NavigationStack {            VStack {                Button("Settings") {                    activeSheet = .settings                }                Button("Profile") {                    activeSheet = .profile                }                                Button("Help") {                    activeSheet = .help("This is the associated help string for this second view")                }            }            .buttonStyle(.bordered)            .navigationTitle("Enum Sheets")            .sheet(item: $activeSheet) { sheet in                sheet            }        }    }}

iPhone 17 Pro - 2026-06-02 at 11.20.08

The Same Pattern Works with fullScreenCover

One of the benefits of having your enum conform directly to View is that the same approach works with fullScreenCover.

Instead of:

.sheet(item: $activeSheet) { $0 }

You can simply write:

.fullScreenCover(item: $activeSheet) { $0 }

The enum doesn't care whether it is being presented as a sheet or a full-screen cover. It is still responsible for generating the destination view.

struct EnumSheetView: View {    @State private var activeSheet: ActiveSheet?    var body: some View {        NavigationStack {            VStack {                Button("Settings") {                    activeSheet = .settings                }                Button("Profile") {                    activeSheet = .profile                }                                Button("Help") {                    activeSheet = .help                }            }            .buttonStyle(.bordered)            .navigationTitle("Enum Sheets")            .fullScreenCover(item: $activeSheet) { $0 }        }    }}

This flexibility can be particularly useful when requirements change. You may initially present content as a sheet and later decide it should be full-screen. With this pattern, the only change required is swapping the presentation modifier.

.sheet(item: $activeSheet) { $0 }

becomes

.fullScreenCover(item: $activeSheet) { $0 }

Everything else remains exactly the same.

The enum continues to provide:

  • Navigation state
  • Unique identification
  • Destination content

This further demonstrates how powerful it can be to allow the enum to own the presentation logic.

Final Thoughts

Using enums that conform to View is a simple but powerful SwiftUI technique. It allows you to combine navigation state and destination views into a single type, resulting in cleaner, more maintainable code.

The next time you find yourself adding a fourth or fifth Boolean property just to present another sheet, consider replacing them with a single enum. Your future self will thank you.

GitHub Repo:

You can find a the completed source code on GitHub.

https://github.com/StewartLynch/EnumSheets