Delay Issue with onChange Modifier in SwiftUI

I'm getting this error : Picker: the selection "3" is invalid and does not have an associated tag, this will give undefined results. Because new brand doesn't have 3 values and .onChange modifier is not working fast enough. Thanks for your help.

Picker("Marka", selection: $brandIndex) {
                    Text("Seçin").tag(0)
                    ForEach(cars.indices, id: \.self) {
                        Text(cars[$0].brand).tag($0 + 1)
                    }
                }
                .onChange(of: brandIndex) {
                    if modelIndex != 0 {
                        modelIndex = 0
                    }
                }
                Picker("Model", selection: $modelIndex) {
                    Text("Seçin").tag(0)
                    if brandIndex != 0 {
                        let _ = print(modelIndex)  // I'm getting first 3 then 0. And I'm getting error.
                        ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                            Text(cars[brandIndex - 1].models[$0])
                                .tag($0 + 1)
                        }
                    }
                }

Accepted Reply

@ FiratSirin You're right, you need an additional test:

        if brandIndex != 0 && modelIndex <= cars[brandIndex - 1].models.count {   // <<-- test modelIndex
            Picker("Model", selection: $modelIndex) {
                Text("Model Seçin").tag(0)
                //            if brandIndex != 0 {
                let _ = print("modelIndex", modelIndex)  // I'm getting first 3 then 0. And I'm getting error.
                ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                    Text(cars[brandIndex - 1].models[$0])
                        .tag($0 + 1)
                }
            }
        } else {
            Text("Select a brand")
        }

Here is the complete code I tested:

struct Car: Decodable, Identifiable {
    enum CodingKeys: CodingKey {
        case brand
        case models
    }
    
    var id = UUID()
    var brand: String
    var models: [String]
}

struct ContentView: View {
    
    @State var brandIndex = 0
    @State var modelIndex = 0
    
    var cars : [Car] = [
        Car(brand: "VW", models: ["Golf", "Passat", "Up"]),
        Car(brand: "Renault", models: ["Megane", "Twingo", "Zoe"]),
        Car(brand: "Porsche", models: ["Carrera", "Taycan"])
    ]
    
    var body: some View {
        Picker("Marka", selection: $brandIndex) {
            Text("Marka Seçin").tag(0)
            ForEach(cars.indices, id: \.self) {
                Text(cars[$0].brand).tag($0 + 1)
            }
        }
        .onChange(of: brandIndex) {
            print("changing brand", brandIndex)
            if modelIndex != 0 {
                modelIndex = 0
            }
        }
        
        if brandIndex != 0 && modelIndex <= cars[brandIndex - 1].models.count {
            Picker("Model", selection: $modelIndex) {
                Text("Model Seçin").tag(0)
                //            if brandIndex != 0 {
                let _ = print("modelIndex", modelIndex)  // I'm getting first 3 then 0. And I'm getting error.
                ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                    Text(cars[brandIndex - 1].models[$0])
                        .tag($0 + 1)
                }
            }
        } else {
            Text("Select a brand")
        }
    }
}

Replies

You cannot rely on some code being executed fast enough. You have to change some design.

Please show complete code so that we can replay and understand exactly what's happening.

Thank you for your answer @Claude31 . I'm trying to get the vehicle make and model from the user. I have a .json file which has all car brand and models. I'm getting values from viewModel and this is my struct:

struct Car: Decodable, Identifiable {
    enum CodingKeys: CodingKey {
        case brand
        case models
    }
    
    var id = UUID()
    var brand: String
    var models: [String]
}

In the first picker I want to select brand. Then I want to see models according to the brand I chose.

               Picker("Brand", selection: $brandIndex) {
                    Text("Choose").tag(0)
                    ForEach(cars.indices, id: \.self) {
                        Text(cars[$0].brand).tag($0 + 1)
                    }
                }
                Picker("Model", selection: $modelIndex) {
                    Text("Choose").tag(0)
                    if brandIndex != 0 {
                        ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                            Text(cars[brandIndex - 1].models[$0])
                                .tag($0 + 1)
                        }
                    }
                }

First, I select a brand and model, then when I change the brand, if the new brand does not have that value, I get the error I mentioned above. I thought that this situation would be solved if the modelIndex variable was set to 0 every time the brand was changed. Thank you so much.

Let's say you have this data:

Brand A with Model 1, Model 2, Model 3
Brand B with Model 1, Model 2

If your state vars are set so that both pickers are at 0, then the Brand picker and Model pickers will both show "Choose".

If you were to pick Brand A, the Model picker will show "Choose".

If you then select Model 3, you will have Brand A and Model 3 shown in the pickers. At this point, brandIndex == 1 and modelIndex == 3.

Now, when you change the Brand picker to Brand B, the Model picker will try to show the model with the tag 3 (modelIndex), and since there isn't a 3, you get that error.

How about adding the onChange modifier to the Model picker, too?

.onChange(of: brandIndex) {
    modelIndex = 0
}
  • Everything you said is true. However, if you look at my first comment, I already did as you said. Are you sure you read what I wrote and examined the code block carefully?

  • No, you didn't. Put the onChange modifier on the SECOND picker as well.

  • Thank you for your answer. But that's not working :(

Add a Comment

Just move the if brandIndex != 0 { test

        if brandIndex != 0 {
            Picker("Model", selection: $modelIndex) {
                Text("Model Seçin").tag(0)
                //            if brandIndex != 0 {
                let _ = print("modelIndex", modelIndex)  // I'm getting first 3 then 0. And I'm getting error.
                ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                    Text(cars[brandIndex - 1].models[$0])
                        .tag($0 + 1)
                }
            }
        } else {
            Text("Select a brand")
        }
  • That's working if I select brand, then I select none. But I'm getting same error if I change the brand.

  • When the brand changes you HAVE to set the modelindex to 0, or it will be left at the previous value, which is what's causing your problem.

  • Yes, I know. I have to set the modelIndex to 0. When I set this value, the picker first gets previous value (3) after gets 0. As I said in the top. And I'm getting error.

Add a Comment

@ FiratSirin You're right, you need an additional test:

        if brandIndex != 0 && modelIndex <= cars[brandIndex - 1].models.count {   // <<-- test modelIndex
            Picker("Model", selection: $modelIndex) {
                Text("Model Seçin").tag(0)
                //            if brandIndex != 0 {
                let _ = print("modelIndex", modelIndex)  // I'm getting first 3 then 0. And I'm getting error.
                ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                    Text(cars[brandIndex - 1].models[$0])
                        .tag($0 + 1)
                }
            }
        } else {
            Text("Select a brand")
        }

Here is the complete code I tested:

struct Car: Decodable, Identifiable {
    enum CodingKeys: CodingKey {
        case brand
        case models
    }
    
    var id = UUID()
    var brand: String
    var models: [String]
}

struct ContentView: View {
    
    @State var brandIndex = 0
    @State var modelIndex = 0
    
    var cars : [Car] = [
        Car(brand: "VW", models: ["Golf", "Passat", "Up"]),
        Car(brand: "Renault", models: ["Megane", "Twingo", "Zoe"]),
        Car(brand: "Porsche", models: ["Carrera", "Taycan"])
    ]
    
    var body: some View {
        Picker("Marka", selection: $brandIndex) {
            Text("Marka Seçin").tag(0)
            ForEach(cars.indices, id: \.self) {
                Text(cars[$0].brand).tag($0 + 1)
            }
        }
        .onChange(of: brandIndex) {
            print("changing brand", brandIndex)
            if modelIndex != 0 {
                modelIndex = 0
            }
        }
        
        if brandIndex != 0 && modelIndex <= cars[brandIndex - 1].models.count {
            Picker("Model", selection: $modelIndex) {
                Text("Model Seçin").tag(0)
                //            if brandIndex != 0 {
                let _ = print("modelIndex", modelIndex)  // I'm getting first 3 then 0. And I'm getting error.
                ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                    Text(cars[brandIndex - 1].models[$0])
                        .tag($0 + 1)
                }
            }
        } else {
            Text("Select a brand")
        }
    }
}

Thank you sooo much for your answer. That's completely working. I did it like that for design. (I mean else section). And I would be very grateful if you could explain the code. Because it's a little confusing. Thank you again :)

                Picker("Marka", selection: $brandIndex) {
                    Text("Seçin").tag(0)
                    ForEach(cars.indices, id: \.self) {
                        Text(cars[$0].brand).tag($0 + 1)
                    }
                }
                .onChange(of: brandIndex) {
                    modelIndex = 0
                }
                if brandIndex != 0 && modelIndex <= cars[brandIndex - 1].models.count {
                    Picker("Model", selection: $modelIndex) {
                        Text("Seçin").tag(0)
                        ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                            Text(cars[brandIndex - 1].models[$0])
                                .tag($0 + 1)
                        }
                    }
                } else { // This section for design
                    Picker("Model", selection: $modelIndex) {
                        Text("Seçin").tag(0)
                    }
                }

Explanation is simple.

Error comes from the fact that modelIndex used in selection may not be valid because there is no brand selected or because for the selected brand models count is lower than the current selection.

And, as you noticed, the view is redrawn before on change is completed.

By moving the if brandIndex != 0 test upward, you avoid the error when you select 'no brand'. Then, you don't see the picker but the Text asking to select a brand.

But that was not enough. For instance, if you selected Renault Zoe (modelIndex = 3) and then select Porsche (only 2 models), view is updated and fails because modelIndex is still 3 (onChange not yet executed).

With the change,

        if brandIndex != 0 && modelIndex <= cars[brandIndex - 1].models.count {   // <<-- test modelIndex
            Picker("Model", selection: $modelIndex) {
                Text("Model Seçin").tag(0)
                //            if brandIndex != 0 {
                let _ = print("modelIndex", modelIndex)  // I'm getting first 3 then 0. And I'm getting error.
                ForEach(cars[brandIndex - 1].models.indices, id: \.self) {
                    Text(cars[brandIndex - 1].models[$0])
                        .tag($0 + 1)
                }
            }
        } else {
            Text("Select a brand")
        }

and not with:

                } else { // This section for design
                    Picker("Model", selection: $modelIndex) {
                        Text("Seçin").tag(0)
                    }
                }

then when you select Porsche, count is not OK, so you display Text("Select a brand") but you have no time to see, as onChange fires and then you see the Picker selection.

To have time to see it, change the onChange as follows:

        .onChange(of: brandIndex) {
            print("changing brand", brandIndex)
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {  // <<-- To get time to see the Text displayed
                if modelIndex != 0 {
                    modelIndex = 0
                }
            }
        }

If that works, don't forget to close the thread on the correct answer. Otherwise explain the use case and the error.

  • Thank you so much again :)

Add a Comment