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).