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 (e.g. 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()
}
}
}
I.e. 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 even an in-place binding being false does not dismiss the fullScreenCover
:
Replacing the state binding with an in-place custom binding shows my state to be properly propagated and causing a view update. The sheet, however, still stays presented.
struct MyDataView:View {
@EnvironmentObject var state:AppState
var body: some View {
VStack {
// draw stuff
}
.onAppear {
state.loadData()
}
.fullScreenCover(isPresented: .init(
get: {
print("recomputing binding: \(state.showOverlay)")
return state.showOverlay
}, set: { newValue in
state.showOverlay = newValue
}
)) {
LoadingOverlay()
}
}
}
This results in the following log message chronology:
recomputing binding: false
recomputing binding: true
recomputing binding: false
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?