Reputation: 387
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
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())
Text
first to represent the state when entryCat
is nil, that is nothing is selected.tag
to hold the identifier used to set entryCat
entryCat
is optional the tag values needs to be optional as well to match the typeA 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
Reputation: 36807
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