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.