Sheets are an interaction style I particularly enjoy as they effectively control user actions, simplifying interaction logic. In iOS 14, SwiftUI introduced fullCover
, supporting fullscreen sheet presentations, offering developers more choices.
Basic Usage
@State var showView1 = false
@State var showView2 = false
List{
Button("View1"){
showView1.toggle()
}
.sheet(isPresented:$showView1){
Text("View1")
}
Button("View2"){
showView2.toggle()
}
.sheet(isPresented:$showView2){
Text("View2")
}
}
The above code allows popping up corresponding views by clicking different buttons. However, it has two drawbacks:
- If your code requires different views as sheets in multiple places, you need to declare multiple corresponding switch values.
- If your view structure is complex, the code may not trigger the sheet display (a problem existing in iOS 13 and still present in iOS 14). The pattern for this issue isn’t entirely clear yet.
Using Item to Correspond to Different Views
Fortunately, there’s another way to activate sheets:
.sheet(item: Binding<Identifiable?>, content: (Identifiable) -> View)
We can use this to respond to a single activation variable and display the required different views.
struct View1: View {
@Environment(\.presentationMode) var presentationMode
let text: String
var body: some View {
NavigationView {
VStack {
Text(text)
Text("View1")
}
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarLeading) {
Button("cancel") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
}
}
struct View2: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Text("View2")
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarLeading) {
Button("cancel") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
}
}
Prepare two views to be displayed.
struct SheetUsingAnyView: View {
@State private var sheetView: AnyView?
var body: some View {
NavigationView {
List {
Button("View1") {
sheetView = AnyView(View1(text:"Hello world"))
}
Button("View2") {
sheetView = AnyView(View2())
}
}
.listStyle(InsetGroupedListStyle())
.sheet(item: $sheetView) { view in
view
}
.navigationTitle("AnyView")
.navigationBarTitleDisplayMode(.inline)
}
}
}
extension AnyView: Identifiable {
public var id: UUID { UUID() }
}
Through the above code, we can pop up the corresponding view by assigning different values to sheetView
. This solution is very convenient but has two problems:
-
In rare cases, when the app enters the background (with the sheet displayed) and then resumes, it may crash. This issue occurs in iOS 13 and the current iOS 14 (tested up to beta 5), especially when the code’s display hierarchy is complex.
-
The commands are not clear. If there are many parameters for the
sheetView
, your code’s readability could be poor.
Using Reducer Approach to Solve the Problem
For each view, we can build its own mini-state machine following the MVVM approach (similar to my other article about Form).
struct SheetUsingEnum: View {
@State private var sheetAction: SheetAction?
var body: some View {
NavigationView {
List {
Button("view1") {
sheetAction = .view1(text: "Test")
}
Button("view2") {
sheetAction = .view2
}
}
.listStyle(InsetGroupedListStyle())
.sheet(item: $sheetAction) { action in
getActionView(action)
}
.navigationTitle("Enum")
.navigationBarTitleDisplayMode(.inline)
}
}
func getActionView(_ action: SheetAction) -> some View {
switch action {
case .view1(let text):
return AnyView(View1(text: text))
case .view2:
return AnyView(View2())
}
}
}
enum SheetAction: Identifiable {
case view1(text: String)
case view2
var id: UUID {
UUID()
}
}
Compared to directly using AnyView
, the code is slightly more extensive, but it eliminates the crash risk and improves code readability.
Solving the Issue of Some Views Not Activating Sheets ##
Regarding some views failing to activate sheets, my current solution is to bind the parent view’s sheetAction
and activate the sheet through the parent view, passing required data through the enum’s associated values.
Update: In iOS 14, using item to activate sheets in some special cases might cause errors or even crashes when the app (with the sheet open) resumes from the background. Hence, the code for activating sheets has been revised. The updated code is unified in Creating Sheets with Cancel Gesture Control in SwiftUI.