I have an observable class like this for a macOS app:
final class MyModel:ObservableObject {
@Published var objects = (0..<3).compactMap {_ in
MyObject()
}
}
class MyObject {
let name:String
let brand:String
let isVehiche:Bool
}
Inside a view I have something like:
@StateObject var model = MyModel()
List {
ForEach($model.objects, id:\.id) {$object in
HStack {
Toggle("is vehicle", isOn: $object.isVehicle)
Text(object.name)
Text(object.brand)
}
}
}
Notice that the elements in the ForEach
and the array itself, are called with $
.
This works almost perfectly.
If I switch the toggle element of an item using the mouse, the list updates correctly.
But if I have a function triggered by a button elsewhere in the interface, which changes all elements programmatically, like
func switchAllToOn() {
for object in objects {
object.isVehicle = true
}
}
The list does not update at all.
But if I add or delete an item from objects
the list updates.
In resume: the list updates if I modify individually items inside objects
with the mouse, but does not update if I run a code to modify all items programmatically.
I know I can do all kinds of ugly juggling to make this work, like creating and id
for the list and changing it after modifying the array, but I would like to understand why this happens and what is the best approach to solve it.
This is the full code:
import SwiftUI
final class MyModel:ObservableObject {
@Published var objects = (0..<3).compactMap {_ in
MyObject()
}
func switchAllToOn() {
// does not work
// for object in $objects {
// object.isVehicle.wrappedValue = true
// }
}
}
class MyObject:Identifiable {
let name:String
let brand:String
var isVehicle:Bool
init(name: String = "no name", brand: String = "no brand" , isVehicle: Bool = false) {
self.name = name
self.brand = brand
self.isVehicle = isVehicle
}
}
struct ContentView: View {
@StateObject var model = MyModel()
private let columns = [GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())]
var body: some View {
VStack {
Button(action: {
model.switchAllToOn()
}, label: {
Text("CHANGE ALL")
})
LazyVGrid(
columns: columns,
alignment: .center,
spacing: 0,
pinnedViews: []
) {
ForEach($model.objects, id:\.id) {$object in
Toggle("is vehicle", isOn: $object.isVehicle)
Text(object.name)
Text(object.brand)
}
}
}
.padding()
}
}
I've run your code, and after trying things here and there I've found a solution. I've modified the loop to be like this:
// If you have the function inside a View struct
func switchAllToOn() {
for object in $model.objects {
object.isVehicle.wrappedValue = true
}
}
// If you have the function inside the ObservableObject class
func switchAllToOn() {
objectWillChange.send() // Notify SwiftUI about the upcoming change
for object in objects {
object.isVehicle = true
}
}
Basically, the toggles works because they use bindings, that's why they refersh the view instantly. So, I've used bindings in the for loop too and it worked. This works if your function is in a view though. If you happen to have that kind of function in the ObservableObject class you need to call objectWillChange.send() because when you directly modify the property of an item in an array, SwiftUI might not detect the change and update the view accordingly. Let me know it that works for you too!