swiftswiftuiobservablepicker

Adding a Picker to change data in Observable class


I am trying to change a value that is stored in class through a custom structure using a picker. I am trying to make an app that lists course name and grade. The picker is in a HStack with a name. I am not able to change the value of a particular element, (in this example, grade) using a Picker. I am using Observable class as I have multiple views which need the access the class.

Here is some of my code,

struct courseItem: Identifiable{
    var id = UUID()
    let name: String
    let grade: String
}
@Observable
class Courses{
    var items = [courseItem]()
}
struct ContentView: View{
    @State private var courses = Courses()
    let grades = ["A", "B", "C"]
    var body: some View{
        List{
            ForEach(courses.items){ item in 
                HStack{
                    Text(item.name)
                    Spacer()
                    Picker("Grade", selection: item.grade){
                        ForEach(grades, id: \.self){
                            Text($0)
                        }
                    }
                }
            }
     
        }
    }
}

I know item.grade should be binding but I get an error when I do that. Right now I get an error saying the compiler is unable to type check the expression in reasonable time. Any help is much appreciated.


Solution

  • Try this approach using bindings $ as shown in the example code, ...to change the value of a particular element, (in this example, grade)

    struct courseItem: Identifiable {
        let id = UUID()  // <--- here
        var name: String
        var grade: String  // <--- here
    }
    
    @Observable class Courses {
        // <--- for testing
        var items: [courseItem] = [courseItem(name: "name-1", grade: "A"),
                                   courseItem(name: "name-2", grade: "B"),
                                   courseItem(name: "name-3", grade: "C")
        ]
    }
    
    struct ContentView: View {
        @State private var courses = Courses()
        
        let grades = ["A", "B", "C"]
        
        var body: some View{
            List{
                ForEach($courses.items){ $item in   // <--- here
                    HStack{
                        Text(item.name)
                        Spacer()
                        Picker("Grade", selection: $item.grade) {  // <--- here
                            ForEach(grades, id: \.self) {
                                Text($0)
                            }
                        }
                    }
                }
            }
        }
    }
    

    If you want to pass courses to other views, you can use @Bindable var courses: Courses to allow editing of the desired property, such as:

    struct ContentView: View {
        @State private var courses = Courses() // <--- here, only one source of truth data
    
        var body: some View {
            TestOtherView(courses: courses) // <--- here
        }
    }
    
    struct TestOtherView: View {
        @Bindable var courses: Courses  // <--- here
        
        let grades = ["A", "B", "C"]
        
        var body: some View {
            List{
                ForEach($courses.items){ $item in   // <--- here
                    HStack{
                        Text(item.name)
                        Spacer()
                        Picker("Grade", selection: $item.grade) {  // <--- here
                            ForEach(grades, id: \.self) {
                                Text($0)
                            }
                        }
                    }
                }
            }
        }
    }
    

    Note, if you want to pass courses to many other views consider using @Environment(Courses.self) private var courses see the docs at Managing model data in your app.

    For example:

    struct ContentView: View{
        @State private var courses = Courses()
    
        var body: some View{
            TestOtherView()
                .environment(courses) // <--- here
        }
    }
    
    struct TestOtherView: View{
        @Environment(Courses.self) private var courses  // <--- here
        let grades = ["A", "B", "C"]
        
        var body: some View{
            @Bindable var courses = courses  // <--- here
            List{
                ForEach($courses.items){ $item in   // <--- here
                    HStack{
                        Text(item.name)
                        Spacer()
                        Picker("Grade", selection: $item.grade) {  // <--- here
                            ForEach(grades, id: \.self) {
                                Text($0)
                            }
                        }
                    }
                }
            }
        }
    }