You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ObservableObject数组内的布尔值无法触发SwiftUI重渲染

解决SwiftUI嵌套ObservableObject变更不触发UI更新问题

问题描述

在SwiftUI项目中,修改嵌套在GridViewModel中的Collection对象内部属性(如isCollapsed)时,UI不会自动更新;只有修改GridViewModel第一层内容(如添加集合)时,才能触发重渲染。演示代码如下:

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 {
    @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()     // 不触发UI更新
                    })
                    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()   // 有效触发更新
    }
    
    func changeItem() {
        gridViewModel.collections[0].items[0] = "---edit------edit------edit---"
        print("done: changed to: \(gridViewModel.collections[0].items[0])")
        gridViewModel.objectWillChange.send()   // 有效触发更新
    }
}

#Preview {
    var gridViewModel = GridViewModel()
    return ContentView().environmentObject(gridViewModel)
    #if os(macOS)
        .frame(width: 700, height: 500)
    #endif
}

可行解决方案

方案1:拆分子视图,用@ObservedObject监听Collection

SwiftUI仅监听当前视图中标记为@ObservedObject/@StateObject的ObservableObject变更。将单个Collection的UI拆分为独立子视图,在子视图中用@ObservedObject持有Collection,即可感知其内部属性变化并触发更新。

示例代码:

// 新增子视图
struct CollectionView: View {
    @ObservedObject var collection: Collection
    
    var body: some View {
        VStack(alignment: .leading) {
            Button("toggle visibility for:", action: {
                collection.isCollapsed.toggle()
            })
            Button("edit", action: {
                collection.items[0] = "---edit------edit------edit---"
            })
            Text(collection.title)
            
            if collection.isCollapsed {
                ForEach(collection.items, id: \.self) { item in
                    Text(item)
                }
            }
            Spacer(minLength: 20)
        }
    }
}

// 修改ContentView中的ForEach部分
ScrollView {
    ForEach(gridViewModel.collections, id: \.self) { coll in
        CollectionView(collection: coll)
    }
}

方案2:让GridViewModel转发Collection的变更通知

在GridViewModel中订阅每个Collection的objectWillChange事件,当Collection内部属性变更时,主动触发GridViewModel的更新通知,让SwiftUI感知到需要刷新UI。

示例代码:

import Combine // 需要引入Combine框架

class GridViewModel: ObservableObject {
    @Published var collections : [Collection] = []
    private var cancellables = Set<AnyCancellable>() // 存储订阅,防止被提前释放
    
    func addCollection(_ collection: Collection) {
        collections.append(collection)
        // 订阅Collection的变更事件,转发给自身的objectWillChange
        collection.objectWillChange.sink { [weak self] _ in
            self?.objectWillChange.send()
        }.store(in: &cancellables)
    }
}

// 修改addCollection方法
func addCollection() {
    let collectionNew = Collection(title: "new_vierte Collection", items: ["apple", "banana", "citrus"])
    gridViewModel.addCollection(collectionNew)
}

// 修改onAppear中的初始化逻辑
.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.addCollection(collection1)
    gridViewModel.addCollection(collection2)
    gridViewModel.addCollection(collection3)
}

方案3:将Collection改为值类型(struct)

如果业务逻辑允许,把Collection从ObservableObject改为普通struct。SwiftUI会自动监听值类型的变化——当struct的属性修改时,整个struct会被视为变更,从而触发UI更新。

示例代码:

// 修改Collection为struct
struct Collection: Identifiable, Hashable {
    static func == (lhs: Collection, rhs: Collection) -> Bool {
        return lhs.title == rhs.title && lhs.items == rhs.items && lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(title)
        hasher.combine(id)
    }
    
    var id = UUID()
    var title : String = ""
    var items : [String] = []
    var isCollapsed: Bool = true
    
    init(title: String, items: [String]) {
        self.title = title
        self.items = items
    }
}

// 修改ContentView中的ForEach,使用绑定语法修改值类型属性
ScrollView {
    ForEach($gridViewModel.collections) { $coll in
        Button("toggle visibility for:", action: {
            $coll.isCollapsed.toggle()
        })
        Button("edit", action: {
            $coll.items[0] = "---edit------edit------edit---"
        })
        Text(coll.title)
        
        if coll.isCollapsed {
            ForEach(coll.items, id: \.self) { item in
                Text(item)
            }
        }
        Spacer(minLength: 20)
    }
}

方案选择建议

  • 若Collection包含复杂业务逻辑(如异步操作、内部状态管理),优先选择方案1或方案2;
  • 若业务逻辑简单,方案3更简洁,利用SwiftUI原生的値类型监听机制,减少ObservableObject的复杂度。

内容的提问来源于stack exchange,提问作者blendstylez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 08:52:30