Home > Software design >  Unable to change color of the button
Unable to change color of the button

Time:12-22

Below is a code I wrote to just change color of a button when pressed. I've been using Flexible grid layout. For some reason, when I click on any of the button, the color does not change. It appears that the StudentRegister class is not being updated. Appreciate any help.

struct StudentView: View {
    
    @State var students: [StudentRegister] = [student1, student2]
    
    let layout = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        LazyVGrid(columns: layout, spacing: 20) {
            ForEach(students, id: \.self) { student in
                VStack() {
                    Button(action: {
                        student.status = Color.green
                    }) {
                        Text(student.name!)
                    }
                    .foregroundColor(student.status!)
                }
            }
        }
    }
}


class StudentRegister: ObservableObject, Hashable, Equatable {
    var name: String?
    @Published var status: Color?
    
    static func == (lhs: StudentRegister, rhs: StudentRegister) -> Bool {
        return lhs.name == rhs.name
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
    
}

CodePudding user response:

Edit: Here's some quick code - But I haven't checked that it compiles or works.

Solution

struct StudentsView: View {
    @StateObject var studentRegister = StudentRegister()
    @State private var isLoading = true
    
    let layout = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        Group {
            if isLoading {
                Text("Loading...")
            } else {
                LazyVGrid(columns: layout, spacing: 20) {
                    ForEach(studentRegister.students, id: \.self) { student in
                        StudentView(student: student)
                    }
                }
            }
        }
        .onAppear {
            // Note: This could obviously be improved with
            // asynchronous-loading in the future.
            studentRegister.load()
            isLoading = false
        }
    }
}

class StudentRegister: ObservableObject {
    @Published var students = [Student]()

    func load() {
        students = [.init("Bob Smith", status: .blue), .init("Alice Davidson", status: .yellow)]
    }
}

struct StudentView: View {
    @ObservedObject var student: Student
    
    var body: some View {
        VStack() {
            Button(action: {
                student.status = Color.green
            }) {
                Text(student.name)
            }
            .foregroundColor(student.status)
        }
    }
}


class Student: ObservableObject, Hashable, Equatable {
    var name: String
    @Published var status: Color
    
    static func == (lhs: StudentRegister, rhs: StudentRegister) -> Bool {
        return lhs.name == rhs.name
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
    
}

Explanation of Solution

Storing the instance of the ObservableObject in the View that "owns" the instance

You should use @StateObject for the property in the highest-level View that holds a particular instance of a class that conforms to the ObservableObject protocol. That's because the View containing that property "owns" that instance.

Receiving an instance of the ObservableObject in lower-level Views

You should use @ObservedObject for the properties in lower-level Views to which that instance is directly passed, or if you choose to pass the instance down to lower-level View's indirectly by passing it as an argument in a call to the environmentObject View Method in the body computed property variable of the View that "owns" the instance, you should use @EnvironmentObject for the properties in the lower-level Views that need to receive it.

What changes cause which ObservableObject's objectWillChange Publishers to fire, and which Views will be re-rendered as a consequence.

If you add, remove, or re-order elements in the studentRegister.students Array, it will cause the StudentRegister instance's objectWillChange Publisher to fire, as its students property is an @Published property, and the adding, removing, or re-ordering of elements in the Array it stores causes the references/pointers to Student instances that that Array contains to change. This in turn will trigger the StudentsView View to be re-rendered, as it's subscribed to that StudentRegister instance's objectWillChange Publisher due to the fact that it's storing a reference to that instance in an @StateObject or @ObservedObject or @EnvironmentObject property (it's specifically storing it in a @StateObject as it happens to "own" the instance).

It's important to note that the studentRegister.students Array is storing references/pointers to Student instances, and hence, changes to properties of any of those Student instances won't cause the elements of the studentRegister.students Array to change. Due to the fact that the changing of one of those Student instance's status properties won't cause the studentRegister.students Array to change, it also won't cause the studentRegister object's objectWillChange Publisher to fire, and hence won't trigger the StudentsView View to be re-rendered.

The changing of one of those Student instance's status properties will cause that Student instance's objectWillChange Publisher to fire though, due to the fact that the status property is an @Published property, and thus changes to the property will trigger the StudentView View to which the Student instance corresponds, to be re-rendered. Remember, like how the StudentsView View is subscribed to the StudentRegister instance's objectWillChange Publisher, the StudentView View is subscribed to its Student instance's objectWillChange Publisher as it's storing a reference to that instance in an @StateObject or @ObservedObject or @EnvironmentObject (it's specifically storing it in an @ObservedObject, as it doesn't "own" the Student instance, but rather is passed it directly by its immediate parent View).

  • Related