SwiftUI: Update data after item in list is selected

vfxdev :

I just spent two days trying to figure this out, hope someone out there can help me. Pretty sure something this basic must be doable and I'm missing some basic concept...

Using XCode 11.4 on macOS, but I suspect this is applicable to iOS as well.

I've cut down the source to the bare minimum to illustrate the problem.

I've got a list of items in a master detail view, where the item list is fetched from one source and once the user clicks on one of them, the details are fetched from a second source and displayed on the right side detail view.

Expected result

When I click on an item, the body of the item is displayed in the detail view on the right.

Actual result

  1. When I click on the first item, the correct body is shown on the right. But the ItemDetailStore.load() method is called twice!

  2. When I click on the second and third item, the detail view remains unchanged.

Source

import SwiftUI

struct ContentView: View {
    @State var selectedItem: Item?

    var body: some View {
        NavigationView {
            ItemList(selectedItem: $selectedItem)
            if selectedItem != nil {
                ItemDetail(selectedItem: $selectedItem)
            }
        }
    }
}

struct ItemList: View {
    @Binding var selectedItem: Item?
    var items = [
        Item(itemId: 0, title: "Item #0", body: "Empty."),
        Item(itemId: 1, title: "Item #1", body: "Empty."),
        Item(itemId: 2, title: "Item #2", body: "Empty.")
    ]

    var body: some View {
        List(selection: $selectedItem) {
            ForEach(Array(items.enumerated()), id: \.element) { index, item in
                ItemRow(item: item)
          }
        }
        .listStyle(SidebarListStyle())
    }
}

struct ItemRow: View {
    var item: Item

    var body: some View {
        Text("\(item.title)")
            .padding(12)
    }
}

struct ItemDetail: View {
    @EnvironmentObject var itemDetailStore: ItemDetailStore
    @Binding var selectedItem: Item?

    var body: some View {
        VStack {
            if itemDetailStore.items.count > 0 {
                Text(itemDetailStore.items[0].body)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onAppear {
            if self.selectedItem != nil {
                self.itemDetailStore.load(id: self.selectedItem!.itemId)
            }
        }
    }
}

struct Item: Codable, Hashable {
    var itemId: Int
    var title: String
    var body: String
}

class ItemDetailStore: ObservableObject {
    @Published var items = [Item]()

    var itemsList = [
        Item(itemId: 0, title: "Item #0", body: "This is an excellent item #0."),
        Item(itemId: 1, title: "Item #1", body: "This is an excellent item #1."),
        Item(itemId: 2, title: "Item #2", body: "This is an excellent item #2.")
    ]

    func load(id: Int) {
        self.items = [itemsList[id]]
        print("\(self.items[0].title) loaded.")
    }
}

And in the AppDelegate I inject the environment:

let contentView = ContentView().environmentObject(ItemDetailStore())

The culprit seems to be the onAppear modifier in ItemDetail, as it's only called when the first item in the list is clicked. Shouldn't the view be updated each time selectedItem changes?

Any feedback is highly appreciated.

Chris :

check this out:

import SwiftUI


struct ContentView: View {

    @EnvironmentObject var itemDetailStore: ItemDetailStore

    var body: some View {
        NavigationView {
            ItemList()
            if self.itemDetailStore.selectedItem != nil {
                ItemDetail()
            }
        }
    }
}

struct ItemList: View {

    @EnvironmentObject var itemDetailStore: ItemDetailStore

    var items = [
        Item(itemId: 0, title: "Item #0", body: "Empty."),
        Item(itemId: 1, title: "Item #1", body: "Empty."),
        Item(itemId: 2, title: "Item #2", body: "Empty.")
    ]

    var body: some View {
        List(selection: self.$itemDetailStore.selectedItem) {
            ForEach(Array(items.enumerated()), id: \.element) { index, item in
                ItemRow(item: item)
          }
        }
        .listStyle(SidebarListStyle())
    }
}

struct ItemRow: View {
    var item: Item

    var body: some View {
        Text("\(item.title)")
            .padding(12)
    }
}

struct ItemDetail: View {
    @EnvironmentObject var itemDetailStore: ItemDetailStore

    var body: some View {
        VStack {
            if itemDetailStore.items.count > 0 {
                Text(itemDetailStore.items[0].body)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onAppear {

        }
    }
}

struct Item: Codable, Hashable, Identifiable {
    var id = UUID().uuidString

    var itemId: Int
    var title: String
    var body: String
}

class ItemDetailStore: ObservableObject {
    @Published var items = [Item]()
    @Published var selectedItem: Item? {
        didSet {
            if self.selectedItem != nil {
                self.load(id: self.selectedItem!.itemId)
            }
        }
    }

    var itemsList = [
        Item(itemId: 0, title: "Item #0", body: "This is an excellent item #0."),
        Item(itemId: 1, title: "Item #1", body: "This is an excellent item #1."),
        Item(itemId: 2, title: "Item #2", body: "This is an excellent item #2.")
    ]

    func load(id: Int) {
        self.items = [itemsList[id]]
        print("\(self.items[0].title) loaded.")
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(ItemDetailStore())
    }
}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=398795&siteId=1