After further investigation, I have confirmed the following (submitted as FB12348064):
The AppStorage property wrapper uses KVO to observe UserDefaults. This makes writing to UserDefaults unsafe on non-main threads, even if the AppStorage is not using the same key as is being written. Any write to UserDefaults causes a lookup of all observers. If the AppStorage is deallocated during that lookup, there will be a crash.
The following reliably crashes for me within a few hundred iterations, tested with both Xcode 14.3.1 and 15b1:
import SwiftUI
struct ContentView: View {
@State var n: Int = 0
// This song-and-dance is to make sure that AppStorageView is destroyed and recreated.
// The two views are visually identical to make the output easier to read. One has @AppStorage,
// the other does not. This causes very fast registering/deregistering from UserDefaults KVO.
func bodyView() -> AnyView {
if n % 2 == 0 {
return AnyView(
ForEach(0..<10) { _ in
AppStorageView(n: n)
})
} else {
return AnyView(
ForEach(0..<10) { _ in
NoAppStorageView(n: n)
})
}
}
var body: some View {
bodyView()
.task {
// Churn UserDefaults on a background thread.
Task.detached {
while true {
UserDefaults.standard.set(Date(), forKey: "randomOtherUserDefaultsKey")
await Task.yield()
}
}
// At the same time, churn the Views to create and destroy AppStorage observations.
while true {
n += 1
await Task.yield()
}
}
}
}
// View with @AppStorage observation
struct AppStorageView: View {
var n: Int
@AppStorage("appStorageValue") var appStorageValue = false
var body: some View { LogView(n: n) }
}
// View without @AppStorage observation
struct NoAppStorageView: View {
var n: Int
var body: some View { LogView(n: n) }
}
struct LogView: View {
var n: Int
var body: some View {
HStack {
Text("App Storage Test: \(n)")
Spacer()
}
}
}
Topic:
App & System Services
SubTopic:
General
Tags: