简体   繁体   中英

SwiftUI/iOS 15: fullScreenCover not dismissing with binding on published var

My App uses SwiftUI, targets iOS 15+ and is governed by an ObservableObject , AppState , which serves as the source of truth throughout the app. I use @Published var s to manage the state of overlays (eg while loading data) in various places without problems.

Simplified AppState :

class AppState:ObservableObject {
  @Published var showLoadingOverlay = false

  func loadData() {
    self.showLoadingOverlay = true
    // fech data
    self.showLoadingOverlay = false
  }
}

Within views I inject the AppState via @EnvironmentObject and use the published variables to control fullScreenCover .

Simplified example:

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    ScrollView {
      VStack {
        ForEach(Array(zip(state.dataSets.indices, state.dataSets)), id: \.0) { (dataSetIndex, dataSet) in
          MirrorChartView(dataSet: dataSet)
        }
      }
    }
    .onAppear {
      state.loadData()
    }
    .fullScreenCover(isPresented: $state.showLoadingOverlay) {
      LoadingOverlay()
    }
  }
}

The problem I'm encountering is that one overlay remains shown while the binding value changes to false . This "getting stuck" only occurs in cases where loadData() completes very fast and the overlay would virtually get dismissed before being fully shown.

Adding a debug monitor to the view confirms that the binding to AppState is properly propagated:

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    VStack {
      // draw stuff
    }
    .onAppear {
      state.loadData()
    }
    .onChange(of: state.showLoadingOverlay) { showOverlay in
      print("show overlay: \(showOverlay)")
    }
    .fullScreenCover(isPresented: $state.showLoadingOverlay) {
      LoadingOverlay()
    }
  }
}

Ie I can see the log show overlay: false while the overlay is sliding up, but it doesn't get dismissed.

Even adding an indirection via a @State local binding does not reliably fix the issue:

struct MyDataView:View {
  @EnvironmentObject var state:AppState
  @State var showLoadingOverlay = false

  var body: some View {
    VStack {
      // draw stuff
    }
    .onAppear {
      state.loadData()
    }
    .onChange(of: state.showLoadingOverlay) { showOverlay in
      self.showLoadingOverlay = showOverlay
    }
    .fullScreenCover(isPresented: self.$showLoadingOverlay) {
      LoadingOverlay()
    }
  }
}

Update: more testing has shown that the following hack does not reliably resolve the issue

I have found that replacing the state binding with an in-place custom binding reliably results in correct behaviour. However, this creates a binding on every render and just feels wrong:

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    VStack {
      // draw stuff
    }
    .onAppear {
      state.loadData()
    }
    .fullScreenCover(isPresented: .init(get: { state.showLoadingOverlay }, set: { _ in })) {
      LoadingOverlay()
    }
  }
}

Update: further findings

I'm not sure what to make of it, but adding a log message to the LoadingOverlay 's .onAppear() shows, that it appears after the binding has changed to false:

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    ScrollView {
      VStack {
        ForEach(Array(zip(state.dataSets.indices, state.dataSets)), id: \.0) { (dataSetIndex, dataSet) in
          MirrorChartView(dataSet: dataSet)
        }
      }
    }
    .onAppear {
      state.loadData()
    }
    .onChange(of: state.showLoadingOverlay) { show in
      print("show loading overlay \(show)")
    }
    .fullScreenCover(isPresented: $state.showLoadingOverlay) {
      LoadingOverlay()
        .onAppear {
          print("loading overlay appears")
        }
    }
  }
}

This results in the following log message chronology:

show loading overlay: true
show loading overlay: false
loading overlay appears

What ways are there to resolve this issue?

Try loading the data in the onAppear() of LoadingOverlay . That way, you delay the loading and may prevent whatever condition is causing the issue:

import SwiftUI

class AppState: ObservableObject {
    @Published var showLoadingOverlay = false
    
    func loadData() {
        //load data
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.showLoadingOverlay = false
        }
    }
}

struct LoadingOverlay: View {
    @EnvironmentObject var state: AppState
    var body: some View {
        VStack {
            Text("Loading")
        }
        .onAppear {
            state.loadData()
        }
    }
}

struct ContentView: View {
    @StateObject var state = AppState()
    var body: some View {
        VStack {
            Text("Data View")
        }
        .onAppear {
            state.showLoadingOverlay = true
        }
        
        .fullScreenCover(isPresented: $state.showLoadingOverlay) {
            LoadingOverlay()
        }
        .environmentObject(state)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM