Multiple sheet view modifiers on the same view

I'm trying to apply multiple sheet view modifiers on a root view powered by a boolean bindings that are evaluated so that only one modal presentation will be in effect at a time. Here is a sample code:

struct ContentView: View {
    @State var counter: Int = 0
    @State var isPresentingA: Bool = false
    @State var isPresentingB: Bool = false

    var body: some View {
        VStack {
            HStack {
                Button("-") { self.counter -= 1 }
                Text("\(self.counter)")
                Button("+") { self.counter += 1 }
            }
            Button("Modal") {
                self.isPresentingA = self.counter % 2 == 0
                self.isPresentingB = self.counter % 2 != 0
            }
        }
        .sheet(isPresented: $isPresentingA) {
            Text("A")
        }
        .sheet(isPresented: $isPresentingB) {
            Text("B")
        }
    }
}

With this code the first sheet is never presented, only the last presentation actually happens. When adding a breakpoint into the content closure of the first sheet modifier I see that it is called as expected but no screen is presented. When counter is updated and a button is pressed again the second modal screen is presented as expected.

Question: is it expected or a bug and if it's expected then why? What's the best practice for applying sheet modifier in general, should it be applied on controls triggering the presentation (in that case same button would trigger both presentations any way and the result will be the same)?

My final goal is to abstract navigation details out of the view presenting the content, that's why the modifiers are applied on the whole view rather than to its different children.

Workaround: instead of applying sheet modifier on the root view apply it on empty buttons put with a root view in a z-stack

    var body: some View {
        ZStack {
            VStack { ... }

            Button(action: {}) { EmptyView() }
            .sheet(isPresented: $isPresentingA) {
                Text("A")
            }

            Button(action: {}) { EmptyView() }
            .sheet(isPresented: $isPresentingB) {
                Text("B")
            }
        }
    }
2 Likes

Most of the time, I'd do this:

enum SheetChoice: Hashable, Identifiable {
  case a, b

  var id: SheetChoice { self }
}

struct ContentView: View {
  @State var sheetState: SheetChoice?

  var body: some View {
    VStack {
      ...
    }
    .sheet(item: $sheetState) { item in
      if item == .a {
        Text("A")
      } else {
        Text("B")
      }
    }
  }
}

Thanks, this will work but this is not the design I’d want to achieve where the child view presenting content should expose opaque signals for presentation logic, so the parent shouldn’t deconstruct those conditions. Having a separate piece of state just to track modal presentation feels like going back to uikit

To me, using separate tracking state doesn’t make much sense since it allows for invalid state like

isPresentingA == true
isPresentingB == true

You could of course proof that your own program disallow such state, but then you’re not taking advantage of the type system.

True, but note that this code is for demonstration purposes only and the state management is not the source of the described problem. Instead of separate boolean bindings it can be whatever else, including enum as in your example.

If it’s the same variable underneath, all the more reason to use that, no? This structure just enforce that only one sheet can be active at a time at a type-level, which is what the two sheet design couldn’t.

In any case, no, I don’t know how to do it with two sheets. The outer modifier overrides the inner ones.

Check out my answer on stackoverflow how to 'kinda' support multiple modals.

Ended up with the same API myself, just with ZStack instead of background, using a background is a nice way :+1: Still wondering though why it behaves like that out of the box, specifically creating a view and discarding it right away without presenting.