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 get
ter always prints the correct value. Adding a breakpoint to the binding's get
ter 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.