mergesort / Boutique

✨ A magical persistence library (and so much more) for state-driven iOS and Mac apps ✨
https://build.ms/boutique/docs
MIT License
899 stars 43 forks source link

[Question] Update SwiftUI List when adding or removing an item from the store #49

Closed flexaddicted closed 1 year ago

flexaddicted commented 1 year ago

Hi all,

maybe this is a dumb question but I would like to update automatically a list every time an item is inserted or removed. This is the sample code I'm using but nothing happens.

As far I understood, Store is an ObservableObject while items is a Published, but it's not a @StateObject hence the update will not work.

Any hint on how to overcome this issue?

Thanks, Lorenzo

extension SampleStruct {
    static let preview = [SampleStruct(value: "value")]
}

extension Store where Item == SampleStruct {
    // Initialize a Store to save our images into
    static let itemsStore = Store<SampleStruct>(
        storage: SQLiteStorageEngine.default(appendingPath: "Items")
    )
}

struct ContentView: View {

    @Stored(in: .itemsStore) private var items

    var body: some View {
        VStack {
            List($items.items) { item in
                Text(item.value)
            }
            Button("Add") {
                Task {
                    do {
                        try await $items.insert(SampleStruct(value: "My Sample Value"))
                    } catch {
                        print(error)
                    }
                }
            }
        }
        .padding()
    }
}
mergesort commented 1 year ago

Hi there again @flexaddicted! The problem is here is that your source of truth for this data (the @Stored property) is in the view that's rendering the data itself. A @State or @StateObject property signals to a SwiftUI view that it should be redrawn when a change occurs, but @Stored isn't a DynamicProperty so it doesn't make that promise.

The solution is to create an ObservableObject class that holds your @Stored property, I tend to call these objects Controllers. (As I've written about extensively here.) My suggestion would be to create a Controller-type object (you can call it whatever you'd like) to do the actual work of manipulating the Store, and your ContentView would hold onto that ObservableObject as a reference using @StateObject.

If you need a reference for how to accomplish this the Boutique Demo project can serve as a model. In it I have an ImagesController which is used as a @StateObject inside of the RedPandaCardView, thus creating a system where changes to the Store propagate in realtime.

Hope that helps!

flexaddicted commented 1 year ago

Hello @mergesort! Thank you again for your reply.

Looking at the demo project I've written a solution like that:

final class BookmarksViewModel: ObservableObject {
    @Published private(set) var newsLetters = [NewsLetter]()
    @Stored(in: .newsLettersStore) var storedNewsLetters

    private var cancellables = Set<AnyCancellable>()

    init() {
        $storedNewsLetters.$items.sink { [weak self] newsLetters in
            self?.newsLetters = newsLetters
        }.store(in: &cancellables)
    }
}

used in the following manner:

@StateObject private var viewModel = BookmarksViewModel()

Not sure if this looks like an elegant one or it could become a bottleneck when the number of elements becomes huge.

Any suggestion?

Thanks, Lorenzo

mergesort commented 1 year ago

Like I mentioned in #48 since this is happening in memory it shouldn't be a bottleneck unless you're saving large objects (usually binary files), or some very, very large number of them. Your implementation should be as efficient as what's happening in the Boutique Demo project, so I wouldn't worry about it.

If you'd prefer to assuage your concerns rather than listening to my ruminations, there's actually a Performance Profiler project in the Boutique repo. You could replace the demo models with your own model to test the read/write performance with as many objects as you'd like, or go a step further and generate an interface around it.

Does that help?

flexaddicted commented 1 year ago

Yes, it helps a lot. Closing the issue.