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.
We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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.
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"
}
}
}
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")
})
}
}
}
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.
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.
Please checkout the documentation from Apple doc: sheet(item:ondismiss:content:)
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?
}
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.
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()
}
}