Picker count not updating when array changes

I have created two pickers, one Category picker, and one Item picker, in separate views. Based on excellent input in an earlier question, my Category picker now successfully filters Items in the Item picker and selects the first Item in the selected Category.


On the surface, everything looks OK, but the count of items in the Item picker doesn't update after I select a new Category. It retains the count from the originally initialized Category even though the array it is based on now has more items in it.


Image and Video of below app and issue can be found here:

https://imgur.com/a/eqyrgbF


Is there some syntax issue I am overlooking, or am I approaching it completely wrong?

/*
Situation:
Two pickers, one selecting a category and the other selecting the items in that category.

Challenge:
The second picker shows the right items, but with the Count from the first category.

To see issue, copy below code into ContentView on a single-view app.
*/


import SwiftUI

struct Item: Identifiable {
    var id = UUID()
    var category:String
    var item:String
}

let myCategories:[String] = ["Category 1","Category 2"]

let myItems:[Item] = [
    Item(category: "Category 1", item: "Item 1.1"),
    Item(category: "Category 1", item: "Item 1.2"),
    Item(category: "Category 2", item: "Item 2.1"),
    Item(category: "Category 2", item: "Item 2.2"),
    Item(category: "Category 2", item: "Item 2.3"),
    Item(category: "Category 2", item: "Item 2.4"),
]

class MyObject: ObservableObject {
    
    // Select Category variables
    @Published var selectedCategory:String = myCategories[0]
    @Published var selectedCategoryItems:[Item] = []
    @Published var selectedCategoryInt:Int = 0 {
        didSet {
            selectCategoryActions(selectedCategoryInt)
        }
    }
    // Select Item variables
    @Published var selectedItem:Item = myItems[0]
    @Published var selectedItemInt:Int = 0 {
        didSet {
            selectedItem = selectedCategoryItems[selectedItemInt]
        }
    }
    
    // Initialize values
    init() {
        selectCategoryActions(selectedCategoryInt)
    }
    
    // Actions when selecting a new category
    func selectCategoryActions(_ selectedCategoryInt:Int) {
        selectedCategory = myCategories[selectedCategoryInt]
        
        // Get items in category
        selectedCategoryItems = myItems.filter{ $0.category.contains(selectedCategory)}
        
        // Select item in category
        let selectedItemIntWrapped:Int? = myItems.firstIndex { $0.category == selectedCategory }
        if let selectedItemInt = selectedItemIntWrapped {
            self.selectedItem = myItems[selectedItemInt]
        }
    }
}



// Picker for selecting a category
struct SelectCategory: View {
    var myCategories:[String]
    @Binding var selectedCategoryInt:Int
    var body: some View {
        VStack {
            Picker(selection: self.$selectedCategoryInt, label: Text("Select category")) {
                ForEach(0 ..< self.myCategories.count, id: \.self) {
                    Text("\(self.myCategories[$0])")
                }
            }.labelsHidden()
        }
    }
}

// Picker for selecting items within the category
struct SelectItem: View {
    var selectedCategoryItems:[Item]
    @Binding var selectedItemInt:Int
    var body: some View {
        VStack {
            // MARK: Something is going wrong here. I don't get right Item ID's
            Picker(selection: self.$selectedItemInt, label: Text("Select item")) {
                ForEach(0 ..< self.selectedCategoryItems.count, id: \.self) {
                    Text("\(self.selectedCategoryItems[$0].item)")
                }
                /*ForEach(selectedCategoryItems, id: \.id) { item in
                    Text("\(item.content)")
                }*/
            }.labelsHidden()
        }
    }
}

struct ContentView: View {
    @ObservedObject var myObject = MyObject()

    var body: some View {
        VStack(alignment: .center, spacing: 10) {
            Text("When selecting Category 2, only two of the four items are shown.")
            Text("Selected category: \(myObject.selectedCategory)")
            Text("Items in category: \(myObject.selectedCategoryItems.count)")
            Text("Selected item: \(myObject.selectedItem.item)")
            SelectCategory(
                myCategories: myCategories,
                selectedCategoryInt: self.$myObject.selectedCategoryInt)
            
            SelectItem(
                selectedCategoryItems: self.myObject.selectedCategoryItems,
                selectedItemInt: self.$myObject.selectedItemInt)
            Spacer()
        }
    }
}

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

Accepted Reply

I solved it myself. It is a bug in the Picker that prevents a refresh of all data when the state changes. The work-around is to update the ID of the picker, which forces a refresh of all data. Obviously it's not a pretty solution, but at least it works until the bug is fixed.


Solution below:

import SwiftUI

// Data
struct Item: Identifiable {
    var id = UUID()
    var category:String
    var item:String
}
let myCategories:[String] = ["Category 1","Category 2"]
let myItems:[Item] = [
    Item(category: "Category 1", item: "Item 1.1"),
    Item(category: "Category 1", item: "Item 1.2"),
    Item(category: "Category 2", item: "Item 2.1"),
    Item(category: "Category 2", item: "Item 2.2"),
    Item(category: "Category 2", item: "Item 2.3"),
    Item(category: "Category 2", item: "Item 2.4"),
]

// Factory
class MyObject: ObservableObject {
    // Category picker variables
    @Published var selectedCategory:String = myCategories[0]
    @Published var selectedCategoryItems:[Item] = []
    @Published var selectedCategoryInt:Int = 0 {
        didSet {
            selectCategoryActions(selectedCategoryInt)
        }
    }
    // Item picker variables
    @Published var selectedItem:Item = myItems[0]
    @Published var selectedItemInt:Int = 0 {
        didSet {
            selectedItem = selectedCategoryItems[selectedItemInt]
        }
    }
    @Published var pickerId:Int = 0
    // Initial category selection
    init() {
        selectCategoryActions(selectedCategoryInt)
    }
    // Actions when selecting a new category
    func selectCategoryActions(_ selectedCategoryInt:Int) {
        selectedCategory = myCategories[selectedCategoryInt]
        // Get items in category
        selectedCategoryItems = myItems.filter{ $0.category.contains(selectedCategory)}
        // Select initial item in category
        let selectedItemIntWrapped:Int? = myItems.firstIndex { $0.category == selectedCategory }
        if let selectedItemInt = selectedItemIntWrapped {
            self.selectedItem = myItems[selectedItemInt]
        }
        self.pickerId += 1 // Hack to change ID of picker. ID is updated to force refresh
    }
}

// View
struct ContentView: View {
    @ObservedObject var myObject = MyObject()
    
    var body: some View {
            VStack(spacing: 10) {
                Section(header: Text("Observable Object")) {
                    Text("Category 2 has four items, but only the first two are shown.")
                    Text("Selected category: \(myObject.selectedCategory)")
                    Text("Items in category: \(myObject.selectedCategoryItems.count)")
                    Text("PickerId updated to force refresh  \(myObject.pickerId)")
                    Text("Selected item: \(myObject.selectedItem.item)")
                    Picker(selection: self.$myObject.selectedCategoryInt, label: Text("Select category")) {
                        ForEach(0 ..< myCategories.count, id: \.self) {
                            Text("\(myCategories[$0])")
                        }
                    }.labelsHidden()
                    // MARK: Something is going wrong here. I don't get right Item ID's
                    Picker(selection: self.$myObject.selectedItemInt, label: Text("Select object item")) {
                        ForEach(0 ..< self.myObject.selectedCategoryItems.count, id: \.self) {
                            Text("\(self.myObject.selectedCategoryItems[$0].item)")
                        }
                    }
                    .labelsHidden()
                    .id(myObject.pickerId) // Hack to get picker to reload data. ID is updated to force refresh.
                }
                Spacer()
            }
    }
}

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

Replies

I solved it myself. It is a bug in the Picker that prevents a refresh of all data when the state changes. The work-around is to update the ID of the picker, which forces a refresh of all data. Obviously it's not a pretty solution, but at least it works until the bug is fixed.


Solution below:

import SwiftUI

// Data
struct Item: Identifiable {
    var id = UUID()
    var category:String
    var item:String
}
let myCategories:[String] = ["Category 1","Category 2"]
let myItems:[Item] = [
    Item(category: "Category 1", item: "Item 1.1"),
    Item(category: "Category 1", item: "Item 1.2"),
    Item(category: "Category 2", item: "Item 2.1"),
    Item(category: "Category 2", item: "Item 2.2"),
    Item(category: "Category 2", item: "Item 2.3"),
    Item(category: "Category 2", item: "Item 2.4"),
]

// Factory
class MyObject: ObservableObject {
    // Category picker variables
    @Published var selectedCategory:String = myCategories[0]
    @Published var selectedCategoryItems:[Item] = []
    @Published var selectedCategoryInt:Int = 0 {
        didSet {
            selectCategoryActions(selectedCategoryInt)
        }
    }
    // Item picker variables
    @Published var selectedItem:Item = myItems[0]
    @Published var selectedItemInt:Int = 0 {
        didSet {
            selectedItem = selectedCategoryItems[selectedItemInt]
        }
    }
    @Published var pickerId:Int = 0
    // Initial category selection
    init() {
        selectCategoryActions(selectedCategoryInt)
    }
    // Actions when selecting a new category
    func selectCategoryActions(_ selectedCategoryInt:Int) {
        selectedCategory = myCategories[selectedCategoryInt]
        // Get items in category
        selectedCategoryItems = myItems.filter{ $0.category.contains(selectedCategory)}
        // Select initial item in category
        let selectedItemIntWrapped:Int? = myItems.firstIndex { $0.category == selectedCategory }
        if let selectedItemInt = selectedItemIntWrapped {
            self.selectedItem = myItems[selectedItemInt]
        }
        self.pickerId += 1 // Hack to change ID of picker. ID is updated to force refresh
    }
}

// View
struct ContentView: View {
    @ObservedObject var myObject = MyObject()
    
    var body: some View {
            VStack(spacing: 10) {
                Section(header: Text("Observable Object")) {
                    Text("Category 2 has four items, but only the first two are shown.")
                    Text("Selected category: \(myObject.selectedCategory)")
                    Text("Items in category: \(myObject.selectedCategoryItems.count)")
                    Text("PickerId updated to force refresh  \(myObject.pickerId)")
                    Text("Selected item: \(myObject.selectedItem.item)")
                    Picker(selection: self.$myObject.selectedCategoryInt, label: Text("Select category")) {
                        ForEach(0 ..< myCategories.count, id: \.self) {
                            Text("\(myCategories[$0])")
                        }
                    }.labelsHidden()
                    // MARK: Something is going wrong here. I don't get right Item ID's
                    Picker(selection: self.$myObject.selectedItemInt, label: Text("Select object item")) {
                        ForEach(0 ..< self.myObject.selectedCategoryItems.count, id: \.self) {
                            Text("\(self.myObject.selectedCategoryItems[$0].item)")
                        }
                    }
                    .labelsHidden()
                    .id(myObject.pickerId) // Hack to get picker to reload data. ID is updated to force refresh.
                }
                Spacer()
            }
    }
}

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

Wouldn't

.id(myObject.selectedItem)

achieve the same, without artifically chanding this pickerID ?

Thanks , your decision is working

This worked for me, based on the same problem. (Thanks for the idea) Created a dummy object class

class DummyObject: ObservableObject { //Forces a change to an object so that it updates
  @Published var pickerId:Int = 0
  init() {
    dummyFunction()
  }
  func dummyFunction() {
    self.pickerId += 1
  }
}

Create an instance of this object in the main view:

@ObservedObject var dummyObject = DummyObject()

Attached that to the picker that needs to be updated using

.id(dummyObject.pickerId)

then created a function that runs when the controlling picker (not the updated picker) is changed and includes the dummy function which updates the id of the picker that needs to be updated and forces it to be re-calculated.

It works, but there must be an easier way..