Pop Up Different Sheets in SwiftUI as Needed

Published on

Get weekly handpicked updates on Swift and SwiftUI!

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

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

  1. If your code requires different views as sheets in multiple places, you need to declare multiple corresponding switch values.
  2. 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:

Swift
.sheet(item: Binding<Identifiable?>, content: (Identifiable) -> View)

We can use this to respond to a single activation variable and display the required different views.

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

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

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

  2. 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).

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

Download the complete project code here

I'm really looking forward to hearing your thoughts! Please Leave Your Comments Below to share your views and insights.

Fatbobman(东坡肘子)

I'm passionate about life and sharing knowledge. My blog focuses on Swift, SwiftUI, Core Data, and Swift Data. Follow my social media for the latest updates.

You can support me in the following ways