In my current project, i am having a hard time getting SwiftUI to recognize a change in a nested ObservableObject, here a demo / debug code
import SwiftUI
import SwiftData
class Collection: ObservableObject, Identifiable, Hashable {
static func == (lhs: Collection, rhs: Collection) -> Bool {
return lhs.title == rhs.title && lhs.items == rhs.items
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
}
var id = UUID()
@Published var title : String = ""
@Published var items : [String] = []
@Published var isCollapsed: Bool = true
init(title: String, items: [String]) {
self.title = title
self.items = items
}
}
class GridViewModel: ObservableObject {
@Published var collections : [Collection] = []
}
struct ContentView: View {
// @EnvironmentObject var gridViewModel : GridViewModel
@StateObject var gridViewModel = GridViewModel()
var body: some View {
VStack {
Text("Toggle Visibility with observed Objects")
Button("Add Collection", action: {
addCollection()
})
ScrollView {
ForEach(gridViewModel.collections, id: \.self) { coll in
Button("toggle visibility for:", action: {
coll.isCollapsed.toggle() // does not work
})
Button("edit", action: {
changeItem()
})
Text(coll.title)
if coll.isCollapsed {
ForEach(coll.items, id: \.self) { item in
Text(item)
}
}
Spacer(minLength: 20)
}
}
}
.onAppear {
let collection1 = Collection(title: "first Collection", items: ["apple", "banana", "citrus"])
let collection2 = Collection(title: "second Collection", items: ["banana", "citrus", "apple"])
let collection3 = Collection(title: "third Collection", items: ["citrus", "banana", "apple"])
gridViewModel.collections = [collection1, collection2, collection3]
}
}
func addCollection() {
let collectionNew = Collection(title: "new_vierte Collection", items: ["apple", "banana", "citrus"])
gridViewModel.collections.append(collectionNew)
gridViewModel.objectWillChange.send() // does work
}
func changeItem() {
gridViewModel.collections[0].items[0] = "---edit------edit------edit---"
print("done: changed to: \(gridViewModel.collections[0].items[0])")
gridViewModel.objectWillChange.send() // works
}
}
#Preview {
var gridViewModel = GridViewModel()
return ContentView().environmentObject(gridViewModel)
#if os(macOS)
.frame(width: 700, height: 500)
#endif
}
As you can see, the change in the first level, which is inside GridViewModel, triggers a re-render, but a change inside Collection does not..
i need SwiftUI to recognize that isCollapsed boolean has changed, and the UI needs to be updated, any suggestions how to make that work?
Try using gridViewModel.objectWillChange.send()
as shown in this code
Button("toggle visibility for:", action: {
gridViewModel.objectWillChange.send() // <--- here
coll.isCollapsed.toggle()
})
EDIT-1:
As mentioned you could also use a struct Collection
to include into the older style class GridViewModel: ObservableObject
, such as:
struct Collection: Identifiable, Hashable { //<--- here
let id = UUID()
var title: String = ""
var items: [String] = []
var isCollapsed: Bool = true
}
class GridViewModel: ObservableObject {
@Published var collections : [Collection] = []
}
struct ContentView: View {
// @EnvironmentObject var gridViewModel : GridViewModel
@StateObject private var gridViewModel = GridViewModel()
var body: some View {
VStack {
Text("Toggle Visibility with observed Objects")
Button("Add Collection") {
addCollection()
}
ScrollView {
ForEach($gridViewModel.collections) { $coll in //<--- here $
Button("toggle visibility for:", action: {
coll.isCollapsed.toggle()
})
Button("edit", action: {
changeItem()
})
Text(coll.title)
if coll.isCollapsed {
ForEach(coll.items, id: \.self) { item in
Text(item)
}
}
Spacer(minLength: 20)
}
}
}
.onAppear {
let collection1 = Collection(title: "first Collection", items: ["apple", "banana", "citrus"])
let collection2 = Collection(title: "second Collection", items: ["banana", "citrus", "apple"])
let collection3 = Collection(title: "third Collection", items: ["citrus", "banana", "apple"])
gridViewModel.collections = [collection1, collection2, collection3]
}
}
func addCollection() {
let collectionNew = Collection(title: "new_vierte Collection", items: ["apple", "banana", "citrus"])
gridViewModel.collections.append(collectionNew)
}
func changeItem() {
gridViewModel.collections[0].items[0] = "---edit------edit------edit---"
print("done: changed to: \(gridViewModel.collections[0].items[0])")
}
}
EDIT-2:
As mentioned in my comments, using the recommended more modern Observable framework
@Observable class Collection: Identifiable { // <--- here
let id = UUID()
var title: String
var items: [String]
var isCollapsed: Bool
init(title: String, items: [String], isCollapsed: Bool = true) {
self.title = title
self.items = items
self.isCollapsed = isCollapsed
}
}
@Observable class GridViewModel { // <--- here
var collections : [Collection] = []
}
struct ContentView: View {
// when passing from parent ... .environment(gridViewModel)
// @Environment(GridViewModel.self) private var gridViewModel
@State private var gridViewModel = GridViewModel() // <---here
var body: some View {
VStack {
Text("Toggle Visibility with observed Objects")
Button("Add Collection") {
addCollection()
}
ScrollView {
ForEach(gridViewModel.collections) { coll in
Button("toggle visibility for:", action: {
coll.isCollapsed.toggle()
})
Button("edit", action: {
changeItem()
})
Text(coll.title)
if coll.isCollapsed {
ForEach(coll.items, id: \.self) { item in
Text(item)
}
}
Spacer(minLength: 20)
}
}
}
.onAppear {
let collection1 = Collection(title: "first Collection", items: ["apple", "banana", "citrus"])
let collection2 = Collection(title: "second Collection", items: ["banana", "citrus", "apple"])
let collection3 = Collection(title: "third Collection", items: ["citrus", "banana", "apple"])
gridViewModel.collections = [collection1, collection2, collection3]
}
}
func addCollection() {
let collectionNew = Collection(title: "new_vierte Collection", items: ["apple", "banana", "citrus"])
gridViewModel.collections.append(collectionNew) //<--- here
}
func changeItem() {
gridViewModel.collections[0].items[0] = "---edit------edit------edit---" //<--- here
print("done: changed to: \(gridViewModel.collections[0].items[0])")
}
}
Note, don't use your static func == ...
and func hash...
, remove them.
Note also, using ForEach(coll.items, id: \.self)
is bad practice,
make sure your coll.items
do not contain multiple same Strings by
using for example a struct ItemName: Identifiable {....}