Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
malhal opened this issue Dec 4, 2020 · 5 comments

Comments

@malhal
Copy link

malhal commented Dec 4, 2020

Hi is there any way that you can prevent this heap allocation made every time a View struct is created that declares a @AppStoreCompat?

_value = Storage(value: value, store: store, key: key, transform: transform)

_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()
    }
}
@xavierLowmiller
Copy link
Owner

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

@malhal
Copy link
Author

malhal commented Dec 4, 2020

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

@xavierLowmiller
Copy link
Owner

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
Copy link
Owner

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

@malhal
Copy link
Author

malhal commented Dec 6, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants