swiftui how to find out if a sheet is currently being presented

2.4k Views Asked by At

I have an app with many nested views, some which show a sheet based on a user action.

But I also have a sheet that I'd like to present on the main view based on a timer (ie, not a user action). But you can't have 2 sheets at the same time, so I'd like to check "something" to see if a sheet is already up, and not present the one from the timer.

I'd like to do this in a general way, and not check every place in the code where a sheet might be presented.

Any suggestions?

3

There are 3 best solutions below

0
On

Ideally there'd be something in the core framework that could be queried to answer the question "Is there a sheet being shown?", but as a commenter pointed out, that is fraught with peril.

So I just decided to leave it alone, that the "default" behavior is fine (ie, it'll defer presenting the sheet until any other sheet is dismissed). In my case this is preferred to any other gyrations.

EDIT:

Eek! I just found out that if the sheet from the timer is popped up while an Alert is showing...it ruins the app. Once you dismiss the alert, any attempt to bring up any sheet anywhere fails. It's as if things got out of sync somewhere. I believe this is similar to:

Lingering popover causes a problem with alerts

If you have alerts in your app, you don't really want to do this.

2
On

Here is how you can handle the sheets - the example below is fully functioning, just pass the view model to the environment before calling TabsView() in the App.

  1. Create an Identifiable object that will handle all the sheets in the program:
// This struct can manage all sheets
struct CustomSheet: Identifiable {

    let id = UUID()
    let screen: TypeOfSheet
    
    // All sheets should fit here
    @ViewBuilder
    var content: some View {
        switch screen {
        case .type1:
            SheetType1()
        case .type2(let text):
            SheetType2(text: text)
        default:
            EmptyView()
        }
    }

    // All types of sheets should fit here
    enum TypeOfSheet {
        case type1
        case type2(text: String)
        case none
    }
}
  1. Create one optional @Published var and one function in the view model; the var will tell the program what sheet is open:
// Code to be included in the view model, so it can
// handle AND track all the sheets
class MyViewModel: ObservableObject {
    
    // This is THE variable that will tell the code whether a sheet is open
    // (and also which one, if necessary)
    @Published var sheetView: CustomSheet?

    func showSheet(_ sheet: CustomSheet.TypeOfSheet) {

        // Dismiss any sheet that is already open
        sheetView = nil

        switch sheet {
        case .none:
            break
        default:
            sheetView = CustomSheet(screen: sheet)
        }
    }
}
  1. Usage:
  • open the sheets by calling the function viewModel.showSheet(...)
  • use .sheet(item:) to observe the type of sheet to open
  • use viewModel.sheet.screen to know what sheet is open
  • sheets can also be dismissed using viewModel.showSheet(.none)
// Example: how to use the view model to present and track sheets
struct TabsView: View {
    @EnvironmentObject var viewModel: MyViewModel
    var body: some View {
        TabView {
            VStack {
                Text("First tab. Sheet is \(String(describing: viewModel.sheetView?.screen ?? .none))")
                    .padding()
                Button("Open sheet type 1") {
                    
                    // Show a sheet of the first type
                    viewModel.showSheet(.type1)
                }
            }
            .tabItem {Label("Tab 1", systemImage: "house")}
            
            VStack {
                Text("Second tab. Sheet is \(viewModel.sheetView == nil ? "Hidden" : "Shown")")
                    .padding()
                Button("Open sheet type 2") {
                    
                    // Show a sheet of the second type
                    viewModel.showSheet(.type2(text: "parameter"))
                }
            }
            .tabItem {Label("Tab 2", systemImage: "plus")}

        }
        
        // Open a sheet - the one selected in the view model
        .sheet(item: $viewModel.sheetView) { sheet in
            sheet.content
                .environmentObject(viewModel)
        }
    }
}

The following code completes the minimal reproducible example:

// Just some sample views for the sheets
struct SheetType1: View {
    @EnvironmentObject var viewModel: MyViewModel
    var body: some View {
        Text("Takes no parameters. Sheet is \(viewModel.sheetView == nil ? "Hidden" : "Shown")")
    }
}
struct SheetType2: View {
    @EnvironmentObject var viewModel: MyViewModel
    let text: String
    var body: some View {
        Text("Takes a string: \(text). Sheet is \(String(describing: viewModel.sheetView?.screen ?? .none))")
    }
}

@main
struct MyApp: App {

    let viewModel = MyViewModel()

    var body: some Scene {
        WindowGroup {
            TabsView()
                .environmentObject(viewModel)
        }
    }
}
0
On

In official document there is an environment isPresented, however it doesn't work.

@Environment(\.isPresented) private var isPresented