SwiftUI:如何阻止@NSManaged属性自动触发不必要的视图更新?
我有一个@NSManaged对象,需要对其进行观察与操作,但不想接收该对象的所有更新通知。举例来说,该对象包含selectedIndex和currentArray两个@NSManaged属性,仅需这两个属性即可存储并重建currentItem。
直接在视图中使用currentArray[currentIndex]存在数组越界风险:若先修改索引再修改数组,或先修改数组再修改索引,触发更新时都可能引发越界异常。因此我选择限制外部直接访问这些属性,转而提供goToNextItem()、changeArrayTo()等封装了内部逻辑的交互接口,并将selectedIndex和currentArray设为私有。
但问题在于,即使将这些属性设为私有,它们的变化仍会触发视图更新。我通过一个简化示例验证了这一点:
final class DataModel:ObservableObject { @Published private var internalCounter = 0 var externalCounter = 0 func incInternalCount() { externalCounter += 1 internalCounter += 1 } } struct Playground: View { @ObservedObject var dataModel = DataModel() var body: some View { Text("Refresh count \(dataModel.externalCounter)") Button { dataModel.incInternalCount() } label: { Text("Increment") } } }
如示例所示,尽管外部无法访问internalCounter,且externalCounter未被@Published修饰,但internalCounter变化时视图仍会刷新。
为更清晰说明实际场景,我补充了一个更复杂的示例:
final class DataModel:ObservableObject { @Published private var itemIndex:Int = 0 @Published private var items:[String] = ["hello", "world"] @Published var currentItem:String var operationsCount = 0 init() { currentItem = "" currentItem = items[itemIndex] } func goToNextItem() { operationsCount += 1 itemIndex += 1 currentItem = items[itemIndex] //Comment this to see problem } func goToPreviousItem() { operationsCount += 1 itemIndex -= 1 currentItem = items[itemIndex] //AND this } func nextItemAvailable() -> Bool { return itemIndex < items.count - 1 } func previousItemAvailable() -> Bool { return itemIndex > 0 } } struct Playground: View { @ObservedObject var dataModel = DataModel() var body: some View { Text("Item: \(dataModel.currentItem) \(dataModel.operationsCount)") HStack { Button { dataModel.goToPreviousItem() } label: { Text("Previous") } .disabled(!dataModel.previousItemAvailable()) Button { dataModel.goToNextItem() } label: { Text("Next") } .disabled(!dataModel.nextItemAvailable()) } } }
在这个示例中,items和itemIndex是用于持久化数据的属性,currentItem用于对外展示内容,同时提供了切换上下项的功能。若注释掉标记的两行代码,私有属性的变化仍会触发视图更新,虽不会导致崩溃,但会影响性能。
因此我想请教:是否有办法让@NSManaged属性不具备@Published特性,避免触发不必要的视图更新?
核心思路
SwiftUI的ObservableObject会通过objectWillChange发送所有@Published属性的变更通知,哪怕属性是私有的。要避免私有@NSManaged属性触发视图更新,需要手动控制objectWillChange的发送时机,只在对外暴露的关键属性变更时通知视图。
方案1:移除私有属性的@Published,手动管理变更通知
对于不需要触发视图更新的私有@NSManaged属性,去掉@Published修饰符;仅在对外暴露的属性(如currentItem)变更时,手动发送objectWillChange通知。
修改后的DataModel示例:
final class DataModel: ObservableObject { // 私有@NSManaged属性去掉@Published @NSManaged private var itemIndex: Int @NSManaged private var items: [String] // 对外属性通过didSet手动触发视图更新 var currentItem: String { didSet { objectWillChange.send() } } var operationsCount = 0 init() { currentItem = items[itemIndex] } func goToNextItem() { guard nextItemAvailable() else { return } operationsCount += 1 itemIndex += 1 // 仅当currentItem变更时,才会触发视图更新 currentItem = items[itemIndex] } func goToPreviousItem() { guard previousItemAvailable() else { return } operationsCount += 1 itemIndex -= 1 currentItem = items[itemIndex] } func nextItemAvailable() -> Bool { return itemIndex < items.count - 1 } func previousItemAvailable() -> Bool { return itemIndex > 0 } }
这样,私有属性itemIndex和items的变更不会触发objectWillChange,只有当currentItem更新时,才会通知视图刷新,完全避免了不必要的更新。
方案2:拆分数据模型,分离持久化层与视图模型
将负责持久化的@NSManaged属性放到单独的Core Data实体类中,视图模型持有这个持久化模型,只对外暴露需要视图展示的属性,并在合适的时机同步数据。
示例结构:
// 持久化模型(Core Data实体) final class CoreDataModel: NSManagedObject { @NSManaged var itemIndex: Int @NSManaged var items: [String] } // 视图模型 final class DataModel: ObservableObject { private let coreDataModel: CoreDataModel @Published var currentItem: String var operationsCount = 0 init(coreDataModel: CoreDataModel) { self.coreDataModel = coreDataModel self.currentItem = coreDataModel.items[coreDataModel.itemIndex] } func goToNextItem() { guard nextItemAvailable() else { return } operationsCount += 1 coreDataModel.itemIndex += 1 currentItem = coreDataModel.items[coreDataModel.itemIndex] } func goToPreviousItem() { guard previousItemAvailable() else { return } operationsCount += 1 coreDataModel.itemIndex -= 1 currentItem = coreDataModel.items[coreDataModel.itemIndex] } func nextItemAvailable() -> Bool { return coreDataModel.itemIndex < coreDataModel.items.count - 1 } func previousItemAvailable() -> Bool { return coreDataModel.itemIndex > 0 } }
这种方式彻底隔离了持久化属性和视图触发逻辑,视图只关心currentItem的变化,完全不受私有持久化属性的影响。
注意事项
- 对于@NSManaged属性,Core Data本身会管理其变更,但如果不需要这些变更触发视图更新,就不要给它们添加@Published。
- 手动发送
objectWillChange时,确保只在真正需要视图刷新的时机调用,避免遗漏必要的更新。
内容的提问来源于stack exchange,提问作者Andriy Gordiychuk

