Логическое значение внутри массива ObservableObject не запускает повторный рендеринг

В моем текущем проекте мне трудно заставить SwiftUI распознать изменение во вложенном ObservableObject, здесь демонстрационный/отладочный код.


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

}

Как видите, изменение на первом уровне, который находится внутри GridViewModel, вызывает повторный рендеринг, но изменение внутри Collection — нет.

мне нужно, чтобы SwiftUI распознал, что логическое значение isCollapsed изменилось, и пользовательский интерфейс необходимо обновить, есть какие-нибудь предложения, как заставить это работать?


68
1

Ответ:

Решено

Попробуйте использовать gridViewModel.objectWillChange.send(), как показано в этом коде.

Button("toggle visibility for:", action: {
    gridViewModel.objectWillChange.send() // <--- here
    coll.isCollapsed.toggle()
})

РЕДАКТИРОВАТЬ-1:

Как уже упоминалось, вы также можете использовать struct Collection включить в старый стиль class GridViewModel: ObservableObject, например:

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])")
    }
    
}

РЕДАКТИРОВАТЬ-2:

Как уже упоминалось в моих комментариях, я использую рекомендованные более современные 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])")
    }
    
}

Обратите внимание: не используйте static func == ... и func hash..., удалите их.

Также обратите внимание: использование ForEach(coll.items, id: \.self) является плохой практикой, убедитесь, что ваш coll.items не содержит несколько одинаковых строк, используя, например, struct ItemName: Identifiable {....}