I recently encountered a difficult-to-diagnose bug in an app that I'm working on, and I thought I would share it with the Apple Developer community.
The app that I'm working on is an iOS app that uses Core Location's Visit Monitoring API, and it is essential that the app is able to process incoming visits while running in the background. However, after real-world testing, I discovered several crash reports which were difficult to understand, as SwiftUI symbols are not symbolicated on debug builds.
Eventually I discovered that the app was crashing when calling an @EnvironmentObject property of the root ContentView from that view's body
when the app was launched directly into the background from not running at all.
After creating a small test app to isolate the problem, I discovered that any environment object declared in the App struct and referenced from the root ContentView causes the crash, with the exception message: "Fatal error: No ObservableObject of type ArbitraryEnvObject found. A View.environmentObject(_:) for ArbitraryEnvObject may be missing as an ancestor of this view."
It seems that when a SwiftUI app is launched in the background, the ContentView's body
is executed, but the environment is not initialized. I searched through as much documentation as I could, but could not find any information about how this should be handled, so I think it's a bug in SwiftUI. I have filed a Feedback Assistant bug report.
The current workaround is to convert my ObservableObject into an object that conforms to the new @Observable protocol, add it to the scene as an Environment Value with the .environment(_:)
modifier rather than the .environmentObject(_:)
modifier, and declare the @Environment property on the view as optional. The object will still be missing when the app is launched in the background, but the optional property can be handled safely to prevent a crash.
Attached to this post is a sample project that demonstrates the issue and the workaround.
And finally, I'd like to hear from anyone who knows more about the expected behavior of background launches of SwiftUI apps, and whether or not there's something I should be doing completely differently.
I'm not able to directly attach a zip archive to this post, so here's an iCloud link to the sample project: BackgroundEnvObjCrash.zip
Thanks for submitting the bug report @squirepinto, Another alternative workaround would be to specify a defaultValue and use the EnvironmentKey. This way there's an instantiated default value available if text
is accessed in the background and you can also set the environment value for a view and all its subviews.
@Observable
final class ArbitraryEnvValue {
var text = "This is text"
}
private struct MyEnvironmentKey: EnvironmentKey {
static let defaultValue: ArbitraryEnvValue = ArbitraryEnvValue()
}
@main
struct BackgroundEnvObjCrashApp: App {
private let locationManager = LocationManager.shared
@State private var envVal = ArbitraryEnvValue()
var body: some Scene {
WindowGroup {
ContentView()
}
.environment(\.myCustomValue, envVal)
}
}
extension EnvironmentValues {
var myCustomValue: ArbitraryEnvValue {
get { self[MyEnvironmentKey.self] }
set { self[MyEnvironmentKey.self] = newValue }
}
}
struct ContentView: View {
@Environment(\.myCustomValue) private var envVal: ArbitraryEnvValue
var body: some View {
.....
}
}