AppStorage
AppStorage copied to clipboard
Heap allocation of Storage class when a View using @AppStorageCompat is recreated during a state change in parent View
Hi is there any way that you can prevent this heap allocation made every time a View struct is created that declares a @AppStoreCompat?
https://github.com/xavierLowmiller/AppStorage/blob/db62f9cd3aec28e901900b9ea71ec38c0665d9f3/Sources/AppStorage/AppStorage.swift#L11 _value = Storage(value: value, store: store, key: key, transform: transform)
E.g. in the code below every time the button is tapped to increment the stateCounter
the body is recomputed which results in ContentView2 being init which is using @AppStoreCompat so causes this heap allocation of a new instance of the Storage class. It seems unnecessary to recreate the Storage class and have it re-register KVO observing when the View hasn't changed since last time. Perhaps there is a way to store the key and use it for equality and then delay creation of the Storage class until DynamicProperty's update call (which I noticed you aren't using).
I should say that Apple's implementation has the same behaviour as yours right now, but I'm sure they will fix this soon.
import SwiftUI
import AppStorage
struct ContentView: View {
@State var stateCounter = 0
var body: some View {
VStack {
Text("\(stateCounter) Hello, world!")
Button("Increment") {
stateCounter = stateCounter + 1
}
ContentView2()
}
.padding()
}
}
struct ContentView2: View {
@AppStorageCompat("counter", store:UserDefaults.group) var storageCounter = 0
var body: some View {
VStack {
Text("\(storageCounter) Hello, world!")
Button("Increment") {
storageCounter = storageCounter + 1
}
}
.padding()
}
}
Perhaps there is a way to store the key and use it for equality and then delay creation of the Storage class until DynamicProperty's update call (which I noticed you aren't using).
Interesting idea! I played around with it during development, but found that I didn't need it initially.
I'll play around with this on the weekend :+1:.
It's easy done with a StateObject. However I suppose that isn't available in the old OS version you were trying to back port AppStorage to. On the other hand if you did use StateObject then you would have a far superior implementation of AppStorage for the current OS!
class SomeObservedObject : ObservableObject {
@Published var counter = 0
}
@propertyWrapper struct Foo: DynamicProperty {
@StateObject var object = SomeObservedObject() // init once no matter how many times the View using it is init
public var wrappedValue: Int {
get {
object.counter
}
nonmutating set {
object.counter = newValue
}
}
}
struct ContentView: View {
@State var counter = 0
var body: some View {
VStack {
Text("\(counter) Hello, world!")
Button("Increment") {
counter = counter + 1
}
ContentView2()
}
.padding()
}
}
struct ContentView2: View {
@Foo var foo
var body: some View {
VStack {
Text("\(foo) Hello, world!")
Button("Increment") {
foo = foo + 1
}
}
.padding()
}
}
StateObject
is something I've looked at for porting to iOS 13 as well, but haven't found the time so far.
Maybe there's a way to implement it differently using an #available
somewhere.
(By the way, I tested using update()
- it gets invoked during every view update as well, so no win there...)
If update()
is called then SwiftUI has already detected a difference and will also run body
next. Try to make the AppStorageCompat
struct be identical every time after its init
. The View
needs to be identical as-well otherwise that would cause a call to body
which would also call update
before hand (I think).