mergesort / Boutique

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

Binding to @Stored items #51

Closed pnewell closed 1 year ago

pnewell commented 1 year ago

I am trying to implement Boutique with SwiftUI and am getting stuck when it comes to updating items. I am developing for iOS 16 (and using the most recent version of Apple's FoodTruck as a starting point):

@MainActor
public class FoodTruckModel: ObservableObject {
    @StoredValue(key: "truck")
    public var truck = Truck()

    @Stored (in: .orderStore) public var orders
    @Stored (in: .donutStore) public var donuts
    @Published public var newDonut: Donut

They have two methods inside FoodTruckModel meant to create bindings for orders/donuts. Donuts is what I am working with:

    public func donutBinding(id: Donut.ID) -> Binding<Donut> {
        Binding<Donut> {
            guard let index = self.donuts.firstIndex(where: { $0.id == id }) else {
                fatalError()
            }
            return self.donuts[index]
        } set: { newValue in
            Task {
                DispatchQueue.main.async {
                    try await self.$donuts.insert(newValue)
                }
            }
        }
    }

I think am using this in my view like this:

struct DonutGallery: View {
    @ObservedObject var model: FoodTruckModel

<OTHER CODE>

    var body: some View {
        ZStack {
            ScrollView {
                ForEach(donuts) { donut in
                    NavigationLink(value: donut.id) {

                        <OTHER CODE SHOWING DONUT>

                    }
                }
            }
        }
        .navigationDestination(for: Donut.ID.self) { donutID in
            DonutEditor(donut: model.donutBinding(id: donutID))
        }
    }
}

When you click on a Donut it takes your to the DonutEditor:

struct DonutEditor: View {
    @Binding var donut: Donut

    var body: some View {

        <OTHER CODE>

            TextField("Name", text: $donut.name, prompt: Text("Donut Name"))

        <OTHER CODE>

    }
}

The problem lies with this textfield. When typing in the field, it "jitters" for lack of a better word.

Here is a recording I made of the behavior (slowed down to make it more apparent): jitter

My guess is it is because the view is getting updated at suboptimal times after typing but before Boutique has inserted the new item.

What is the best way around this, can I just not use Binding at all with Stored?

mergesort commented 1 year ago

Hey @pnewell, sorry that it took me a bit to respond, it's been quite a long couple of weeks.

It's a little hard to tell what's going on without being able to play with the code. If you're still running into this would you mind uploading a zip of your project?

One guess I have is you probably shouldn't be mixing structured concurrency (Task) with GCD (DispatchQueues) in this code.

Task {
    DispatchQueue.main.async {
        try await self.$donuts.insert(newValue)
    }
}

Instead I would use Task { @MainActor in } to dispatch the result onto the main queue, rather than a DispatchQueue. I suspect this might be the source of the jitter, as the update might be waiting until the next run loop to render.

Hope I can help going forward!

mergesort commented 1 year ago

I'm going to close this our assuming it was resolved, if my explanation wasn't helpful please feel free to reopen the issue. 🙂