Galen Smith
Galen Smith

Reputation: 387

Picker selection "0" is invalid and does not have an associated tag

I have a class containing transaction categories (see example code below) from which the user may select during the setup process a subset for use in the app. Later on during transaction entry, if the first category in the class wasn't selected (during setup) and the picker state parameter is initialized to zero, I see the console message "Picker: the selection "0" is invalid and does not have an associated tag, this will give undefined results". In fact if the state parameter is initialized to say 5 and the 5th category wasn't selected during setup it will also give the same warning.

My questions is how to initialize the state parameter so I don't get this picker warning?

From the console message I would think that the problem is with the ForEach loop and not the conditional. But if the conditional if categories.catItem[item].catInUse == true { is removed I don't see the warning.

For example, in the code below if category Cat A is not selected during app setup (catInUse is false) and the transaction entry picker state parameter is @State private var entryCat: Int = 0 I will see the console warning message above. Category Cat A is still in the list--it just isn't displayed.

struct getCategory: View {

    @EnvironmentObject var categories: Categories

    @State private var entryCat: Int = 0

    var body: some View {

        Section(header: Text("Select Category")) {

            Picker(selection: $entryCat, label: Text("")) {
                ForEach(0 ..< categories.catItem.count, id: \.self) { item in
                    if categories.catItem[item].catInUse == true {
                        Text(categories.catItem[item].catName)
                            .bold()
                    }
                }
            }.pickerStyle(MenuPickerStyle())
        }
    }
}



// working categories
struct CatModel:  Codable, Identifiable, Hashable {
    var id = UUID()
    var catNum: Int         // used by setup categories
    var catName: String     // category name
    var catTotal: Double    // category total
    var catBudget: Double   // category budget
    var catPix: String      // sf symbol
    var catInUse: Bool      // catInUse: true = category in use
    var catCustom: Bool     // true = custom category (can be deleted)
}


class Categories: ObservableObject {
    @Published var catItem: [CatModel] {
        didSet {   // save categories
            if let encoded = try? JSONEncoder().encode(catItem) {
                UserDefaults.standard.set(encoded, forKey: StorageKeys.workCat.rawValue)
            }
        }
    }

    var catCount: Int = 0
    init() {

        // read in category data
        if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
            if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
                self.catItem = decoded
                return
            }
        }

        catItem = []

        let item0 = CatModel(catNum: 0, catName: "Cat A", catTotal: 0.0, catBudget: 0, catPix: "a.circle", catInUse: false, catCustom: false)
        self.catItem.append(item0)

        let item1 = CatModel(catNum: 1, catName: "Cat B", catTotal: 0.0, catBudget: 0, catPix: "b.circle", catInUse: false, catCustom: false)
        self.catItem.append(item1)

        let item2 = CatModel(catNum: 2, catName: "Cat C", catTotal: 0.0, catBudget: 0, catPix: "c.circle", catInUse: false, catCustom: false)
        self.catItem.append(item2)

        let item3 = CatModel(catNum: 3, catName: "Cat D", catTotal: 0.0, catBudget: 0, catPix: "d.circle", catInUse: false, catCustom: false)
        self.catItem.append(item3)

        let item4 = CatModel(catNum: 4, catName: "Cat E", catTotal: 0.0, catBudget: 0, catPix: "e.circle", catInUse: false, catCustom: false)
        self.catItem.append(item4)

        let item5 = CatModel(catNum: 5, catName: "Cat F", catTotal: 0.0, catBudget: 0, catPix: "f.circle", catInUse: false, catCustom: false)
        self.catItem.append(item5)
    }
}

Upvotes: 3

Views: 4591

Answers (2)

Joakim Danielson
Joakim Danielson

Reputation: 52043

I would avoid using an index to access the array and instead use a for each loop. And since you might not have any object to preselect if not any category is in use or the one with value 0 is not in use I would recommend to use an optional selection property instead.

It is better to have the type of the selection property to be an identifier rather than the whole type and since your model conforms to Identifiable then the id is a good choice.

@State private var entryCat: CatModel.ID?

Also since you want to filter your model objects I prefer to do that in a computed property but of course doing it directly in the ForEach is also an option

var categoryList: [CatModel] {
    return categories.catItem.filter(\.catInUse)
}

Then the picker code could be changed to

Picker("", selection: $entryCat) {
    Text("<Nothing selected>").tag(nil as CatModel.ID?)
    ForEach(categoryList) { category in
        Text(category.catName)
            .tag(Optional(category.id))
    }
}.pickerStyle(MenuPickerStyle())
  • I use a Text first to represent the state when entryCat is nil, that is nothing is selected.
  • I use tag to hold the identifier used to set entryCat
  • Since entryCat is optional the tag values needs to be optional as well to match the type

A bit off topic but some suggestions regarding naming off types, type names should start with an uppercase letter so I would use GetCategory and a good name for a model is a noun and don't abbreviate it so here I would use Category instead of CatModel

Upvotes: 7

You could try a different approach as shown in my example code, instead of using a range and a if in your ForEach loop. In essence, you need to cater for all possibilities for the selection entryCat including a nil for no selection.

struct ContentView: View {
    @StateObject var categories = Categories()
    
    var body: some View {
        getCategory().environmentObject(categories)
    }
}


struct getCategory: View {
    @EnvironmentObject var categories: Categories
    @State private var entryCat: CatModel? // <-- here
    
    var body: some View {
        Section(header: Text("Select Category")) {
            Picker(selection: $entryCat, label: Text("")) {
                Text("No Category").tag(nil as CatModel?)  // <-- here
                ForEach(categories.catItem.filter({$0.catInUse})) { cat in // <-- here
                    Text(cat.catName).tag(cat as CatModel?).bold() // <-- here
                }
            }.pickerStyle(MenuPickerStyle())
        }
    }
}

Note, all your CatModel in your initial catItem examples, of your class Categories, have catInUse: false, so you will not see any picker entries with that.

Upvotes: 1

Related Questions