Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI @Binding doesn't refresh View

I have a simple master/detail interface where the detail view modifies an item in an array. Using the below, the model is updated properly, but SwiftUI doesn't refresh the View to reflect the change.

Model:

struct ProduceItem: Identifiable {
    let id = UUID()
    let name: String
    var inventory: Int
}

final class ItemStore: BindableObject {
    var willChange = PassthroughSubject<Void, Never>()

    var items: [ProduceItem] { willSet { willChange.send() } }

    init(_ items: [ProduceItem]) {
        self.items = items
    }
}

Master view that displays a list of ProduceItems (an ItemStore is inserted into the environment in the SceneDelegate):

struct ItemList: View {
    @EnvironmentObject var itemStore: ItemStore

    var body: some View {
        NavigationView {
            List(itemStore.items.indices) { index in
                NavigationLink(destination: ItemDetail(item: self.$itemStore.items[index])) {
                    VStack(alignment: .leading) {
                        Text(self.itemStore.items[index].name)
                        Text("\(self.itemStore.items[index].inventory)")
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }
            }
            .navigationBarTitle("Items")
        }
    }
}

Detail view that lets you change the inventory value of an item:

struct ItemDetail: View {
    @Binding var item: ProduceItem

    var body: some View {
        NavigationView {
            Stepper(value: $item.inventory) {
                Text("Inventory is \(item.inventory)")
            }
            .padding()
            .navigationBarTitle(item.name)
        }
    }
}

Tapping on the stepper in the ItemDetail view modifies the item in the store, but the text of the stepper doesn't change. Navigating back to the list confirms the model has been changed. Also, I confirmed that the store calls willChange.send() to its publisher. I would assume that the send() call updates the ItemStore in the environment and the detail view's @Binding property should be notified of the change and refresh the display (but it doesn't).

I tried changing ItemDetail's item property to use @State:

@State var item: ProduceItem = ProduceItem(name: "Plums", inventory: 7)

In this case, the model is item is updated when using the stepper and the view is refreshed, displaying the updated inventory. Can anyone explain why using the @Binding property doesn't refresh the interface, but a local @State property does?

like image 573
smr Avatar asked Jul 20 '19 19:07

smr


1 Answers

Here you have a workaround. Use the index, instead of the element when calling ItemDetail. And inside ItemDetail, you use the @EnvironmentObject.

struct ItemList: View {
    @EnvironmentObject var itemStore: ItemStore

    var body: some View {
        NavigationView {
            List(itemStore.items.indices) { index in
                NavigationLink(destination: ItemDetail(idx: index)) {
                    VStack(alignment: .leading) {
                        Text(self.itemStore.items[index].name)
                        Text("\(self.itemStore.items[index].inventory)")
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }
            }
            .navigationBarTitle("Items")
        }
    }
}

struct ItemDetail: View {
    @EnvironmentObject var itemStore: ItemStore
    let idx: Int


    var body: some View {
        NavigationView {
            Stepper(value: $itemStore.items[idx].inventory) {
                Text("Inventory is \(self.itemStore.items[idx].inventory)")
            }
            .padding()
            .navigationBarTitle(itemStore.items[idx].name)
        }
    }
}
like image 192
kontiki Avatar answered Jan 12 '23 00:01

kontiki