I've been using @AppStorage for a long time, but it came to my attention that there seems to be an issue with how @AppStorage properties work with ObservableObject, or rather with multiple ObservableObject.
Basically, when using a shared @AppStorage property in two or more ObservableObjects, if the property is updated in one of them, the other doesn't receive the information and is effectively desynced with the actual value. Updating the desynced property triggers an update on the Views, but doesn't sync the value with the other ObservableObjects.
I recorded a video of this behavior: https://youtube.com/shorts/kbFM2W0IvRo
Here's my sample project:
struct ContentView: View {
var body: some View {
TabView {
TabAView()
.tabItem {
Label {
Text("From View")
} icon: {
Image(systemName: "square")
}
}
TabAView()
.tabItem {
Label {
Text("From View 2")
} icon: {
Image(systemName: "circle")
}
}
TabBView()
.tabItem {
Label {
Text("From ObservableObj")
} icon: {
Image(systemName: "triangle")
}
}
TabBView()
.tabItem {
Label {
Text("From ObservableObj 2")
} icon: {
Image(systemName: "fireworks")
}
}
}
}
}
struct TabAView: View {
@AppStorage("test") private var increment = 0
var body: some View {
VStack {
Text("\(increment)")
Button {
increment += 1
} label: {
Text("Increment from View")
}
}
}
}
final class TabBModel: ObservableObject {
@AppStorage("test") var increment = 0
}
struct TabBView: View {
@StateObject private var model = TabBModel()
var body: some View {
VStack {
Text("\(model.increment)")
Button {
model.increment += 1
} label: {
Text("Increment from ObservableObject")
}
}
}
}
I filed a feedback to Apple over this issue: FB13250915.
This issue seems to have started from the moment I started building my projects with Xcode 15. I've tried building on 15.1 Beta 1, and also 14.3.1 with the same results.
I have two questions:
- Am I doing anything wrong? I'm at the point of wondering if it's somehow a bad practice to use
@AppStorageproperties in more than oneObservableObject. Or have I missed a glaring issue in the code shown above? - Is anyone affected by this issue as well? What are you doing as of consequence? I'm considering not using
@AppStorageanymore, but I'm not sure what the best alternative would be.
So, I don't have a direct answer to the problem I described, but here's the workaround I found:
Storage Controller
This ObservableObject is my replacement to @AppStorage. It's not as simple, but it works. You add it as a @StateObject to your app and you can pass it down via
EnvironmentObject.You can also use it as a Singleton, and this is the way to use it in other
ObservableObjects.Here it is:
How to use it
In your
Views, you can still rely on @AppStorage, no need to bother with the following.But in your
ObservableObjects, you can do this:Step 1: Declare a property mirroring your storage value
Here, we do three things:
Step 2: Link the property with Combine in
initThis code ensures our property always has the latest value. The
filterpart is crucial because it prevents an infinite loop that thewillSetabove would trigger.Discussion and limits