11

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?

Tajinder
  • 2,248
  • 4
  • 33
  • 54
smr
  • 890
  • 7
  • 25
  • Hi @smr, I have seen something very similar and I posted a question too: https://stackoverflow.com/questions/56969676/bindableobject-async-call-to-didchange-send-does-not-invalidate-its-view-and I still need to see if the problem remains in beta 4, but seeing your question, it probably does :-( – kontiki Jul 20 '19 at 20:17
  • I've been playing around with this for the last week. Last night I found out that NavigationLink (destination....) does not auto update the data it is presenting. I made a second list using the same self.itemStore.items, and it updates automatically when the detail data changes, but the same list on the same page using a NavigationLink did not. – ShadowDES Jul 21 '19 at 18:52
  • Hmmm...seems like a different problem. When I run the code above (without the workaround from @kontiki), it does update the List when navigating back to it (beta 4). Do you get the same? – smr Jul 21 '19 at 20:53

1 Answers1

6

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)
        }
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Yup, that did it. It also fixed the preview which was broken in my version. Thanks for the workaround! – smr Jul 20 '19 at 21:07