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
When I click on the first item, the correct body is shown on the right. But the ItemDetailStore.load() method is called twice!
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.
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())
}
}