AppStorage icon indicating copy to clipboard operation
AppStorage copied to clipboard

Heap allocation of Storage class when a View using @AppStorageCompat is recreated during a state change in parent View

Open malhal opened this issue 3 years ago • 5 comments

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()
    }
}

malhal avatar Dec 04 '20 11:12 malhal

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:.

xavierLowmiller avatar Dec 04 '20 16:12 xavierLowmiller

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()
    }
}

malhal avatar Dec 04 '20 20:12 malhal

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.

xavierLowmiller avatar Dec 05 '20 16:12 xavierLowmiller

(By the way, I tested using update() - it gets invoked during every view update as well, so no win there...)

xavierLowmiller avatar Dec 06 '20 08:12 xavierLowmiller

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).

malhal avatar Dec 06 '20 08:12 malhal