Multiple Sheets in SwiftUI

Sometimes you will need to show different sheets within one SwiftUI view. This can be difficult to manage, but it can be solved using the latest in SwiftUI.

Overview

You have created an interface where your view might contain more then two sheets.

Having multiple sheets within one SwiftUI view can happen. The sheet(item:content:) takes an item: Binding<Identifiable> which means in the latest swift you can use an Enum and switch on it in the content closure.

First Step

Create the enum. It must conform to Identifiable don’t forgot to do this.

// 01 Setup the enum. Identifiable requires and id
enum Activesheet: Identifiable {
   case twitter
   case picker
   case web
    
    var id: String {
       switch self {
        case .twitter:
            return "twitter"
        case .picker:
            return "picker"
        case .web:
            return "web"
       }
    }
}

Second Step

Create the view.


struct TestingView: View {
    
    // 02 Setup @State for sheet
    @State var sheet: Activesheet?
    
    var body: some View {
        VStack {
            buttons
        // 03 Pass in the @State sheet as a binding by using a $
        }.sheet(item: $sheet, onDismiss: didDismiss) { activeSheet in
            switch activeSheet {
            case .twitter:
                Text("Twitter Page")
            case .picker:
                Text("Picker Page")
            case .web:
                Text("Web Info")
            }
        }
    }
    
    private func didDismiss() {
        print("Did dismiss")
        self.sheet = nil
    }
    
    private var buttons: some View {
        Group {
            Button(action: {
                sheet = .twitter
            }, label: {
                Text("Show Twitter")
            })
            Divider()
            Button(action: {
                sheet = .picker
            }, label: {
                Text("Show Picker")
            })
            Divider()
            Button(action: {
                sheet = .web
            }, label: {
                Text("Show Web")
            })
        }
    }
    
    
    
}

Structure

We have a property called private var buttons which contains a group of buttons. I wouldn’t have seperated it out like this, but for this post it’s easier to seperate out the buttons from the main view.

Each button has an action that will define the @State var sheet: Activesheet?. And we also have a func didDismiss() that will reset the sheet to nil once the sheet has actually been dimissed.

In the body we have the .sheet(item: $sheet, onDismiss: didDismiss) which will pass in the activeSheet whenever the sheet gets updated. And then we will switch on each case and use the view that is provided. In these examples, I’m just loading a Text() view but you could provide any type of view.

Preview

Don’t forget to add the PreviewProvider in your project or you might not get it working.

If you want to download and testout this code sample click here to download the gist of the project.

Apple docs

Please checkout the documentation from Apple doc: sheet(item:ondismiss:content:)

Example using @StateObject

If you find yourself using a lot of sheets that are common, you could try to contain it into one object and seperate it out into a ObservableObject and create a ModalManager.

// 01 Enum
enum Activesheet: Identifiable {
   case twitter
   case picker
   case web
    
    var id: String {
       switch self {
        case .twitter:
            return "twitter"
        case .picker:
            return "picker"
        case .web:
            return "web"
       }
    }
}

// 02: Create an Observable Object called `Modal Manager`
class ModalManager: ObservableObject {
    @Published var sheet: Activesheet?
}

Modal Manager

All we have done is created a new ObsverableObject called ModalManager with a single @Published property that will keep track of what modal is active.

Next Step

We are going to recreate our previous views but I’m going to add a @StateObject instead of a @State and add nested views that will reference our modal manager. This should make more sense once you see the layout.

import SwiftUI
import Combine

// 01 Enum
enum Activesheet: Identifiable {
   case twitter
   case picker
   case web
    
    var id: String {
       switch self {
        case .twitter:
            return "twitter"
        case .picker:
            return "picker"
        case .web:
            return "web"
       }
    }
}

// 02: Create an Observable Object called `Modal Manager`
class ModalManager: ObservableObject {
    @Published var sheet: Activesheet?
}



struct ContentView: View {
    
    // 03: @StateObject is a our ModalManager we defined earlier
    @StateObject private var modalManager: ModalManager = ModalManager()

    var body: some View {
        VStack {
            buttons
        // 04 Pass in the Binding using $modalManager.sheet
        }.sheet(item: $modalManager.sheet, onDismiss: didDismiss) { activeSheet in
            switch activeSheet {
            case .twitter:
                Twitter(sheet: $modalManager.sheet) // This is a new view
            case .picker:
                Picker(sheet: $modalManager.sheet)  // This is a new view
            case .web:
                Web(sheet: $modalManager.sheet)  // This is a new view
            }
        }
    }
    
    private func didDismiss() {
           print("Did dismiss")
            self.modalManager.sheet = nil
       }

    // 05 We have our buttons. Nothing new except the action references the stateObject
    private var buttons: some View {
           Group {
               Button(action: {
                modalManager.sheet = .twitter
               }, label: {
                   Text("Show Twitter")
               })
               Divider()
               Button(action: {
                modalManager.sheet = .picker
               }, label: {
                   Text("Show Picker")
               })
               Divider()
               Button(action: {
                modalManager.sheet = .web
               }, label: {
                   Text("Show Web ")
               })
           }
       }
    
}


// 04 We have seperate views, that could contain it's own logic
// each view contains it's own @Binding. 
struct Twitter: View {
    
    @Binding var sheet: Activesheet?
    
    var body: some View {
        Button(action: {
            self.sheet = nil
        }, label: {
            Text("Dismiss Twitter")
        })
    }
}

struct Picker: View {
    
    @Binding var sheet: Activesheet?
    
    var body: some View {
        Button(action: {
            self.sheet = nil
        }, label: {
            Text("Dismiss Picker")
        })
    }
}

struct Web: View {
    
    @Binding var sheet: Activesheet?
    
    var body: some View {
        Button(action: {
            self.sheet = nil
        }, label: {
            Text("Dismiss Web Picker")
        })
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}