fullScreenCover not dismissed if binding changes rapidly.

I'm working on an app targeting iOS 15+ using SwiftUI. The app has several Views that load data from an API in their onAppear() method. While the loading operation is in progress, these views show a loading overlay via .fullScreenCover().

While most of the time this works as expected, I've discovered that if the API operation completes before the overlay's .onAppear() has fired, the overlay gets stuck on screen, i.e. does not dismiss. This bug occurs both in the simulator and on device.

This is a simplified version of my implementation:


struct MyDataView: View {
  @EnvironmentObject var store:Store

  var Content: some View {
    // ...
  }

  @ViewBuilder
  var body: some View {
    let showLoadingOverlay = Binding(
      get: {
        store.state.loading
      },
      set: { _ in }
    )

    Content
      .onAppear {
        store.dispatch(LoadData)
      }
      .fullScreenCover(isPresented: showLoadingOverlay) {
        LoadingOverlay()
      }
  }
}

Log messages tell me that my store is updating correctly, i.e. the booleans all operate as expected. Adding log output to the binding's getter always prints the correct value. Adding a breakpoint to the binding's getter makes the problem disappear.

I've found that the chronology of events that lead to this bug is:

  • MyDataView.onAppear()
  • LoadData
  • Binding: true
  • Overlay starts animating in
  • LoadData finishes
  • Binding: false
  • Overlay fires it's onAppear

I.e. whenever loading finishes before the fullScreenCover's onAppear is fired, the overlay get's stuck on screen. As long as loading takes at least as long as it takes the overlay to appear, the bug does not occur.

It appears to be a race condition between the .fullScreenCover appearing and the binding changing to false.

I've found that the bug can be avoided if loading is triggered in the overlay's .onAppear(). However, I would like to avoid this workaround because the overlay is not supposed to carry out data loading tasks.

Did you ever find a solution to this? We are doing a very similar thing where we have a fullscreenCover that is using a custom binding... where the getter is bound to a piece of state that can change rapidly.

We're seeing similar things in watchOS 11 and iOS 18 where sometimes the fullscreenCover will not dismiss.

Although I abandoned the approach, I did eventually found a solution: Instead of a custom Binding, you should use a @State var showOverlay and listen for changes to your fast-changing state variable via view.onChange(of: fastChangingStateVariable) { value in showOverlay = value == true } or view.onReceive(), if it's a published variable.

@State private var showFullscreenOverlay = false

// ...

private func onStateVariableChange(_ stateVariable: SomeType) {
    self.showFullscreenOverlay = stateVariable == 0 // or so
}

// ...

var body: some View {
  // ...
  .onChange(of: fastChangingStateVariable, perform: onStateVariableChange)
  .fullscreenCover(isPresented: $showFullscreenOverlay) {
    MyOverlay()
  }
}

fullScreenCover not dismissed if binding changes rapidly.
 
 
Q