如何仅在NSManagedObject指定字段变更时重渲染SwiftUI View?
我的App在导入大数据集时,ItemGroupView的重渲染次数远超需求,导致应用卡顿。通过Instruments已确认该View存在过度渲染问题。将itemGroup设为@Observable可能解决问题,但需兼容iOS 16。当前使用.onReceive(itemGroup.publisher(for: \.id))时,无论id是否变更,只要ItemGroup执行save()就会触发回调。由于ItemGroupView数量庞大,即便单次重渲染成本极低,全部重渲染的开销也很高。
如何让View仅在id发生变更时接收事件?
尝试背景
CoreData模型包含两个实体:Item和ItemGroup,均有一个名为id的UUID字段,二者为一对多关系。ItemGroup有一个派生字段count,派生规则为item.@count。
尝试1:直接监听id的Publisher
import SwiftUI import CoreData struct ItemGroupView: View { var itemGroup: ItemGroup var body: some View { let _ = Self._printChanges() Text("\(itemGroup.id!.uuidString)") .onReceive(itemGroup.publisher(for: \.id)) { _ in print("id \(itemGroup.id!.uuidString) changed")} } } struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest(sortDescriptors: [], animation: .default) private var items: FetchedResults<Item> @FetchRequest(sortDescriptors: [], animation: .default) private var itemGroups: FetchedResults<ItemGroup> @EnvironmentObject var persistenceController: PersistenceController @State var nextGroup = 0 var body: some View { NavigationView { ScrollView { Section("Groups") { ForEach(itemGroups) { itemGroup in ItemGroupView(itemGroup: itemGroup) } } Section("Items") { ForEach(items) { item in Text("\(item.id!.uuidString)") } } } .onAppear() { do { if try viewContext.count(for: ItemGroup.fetchRequest()) == 0 { for _ in 0..<2 { let group = ItemGroup(context: viewContext) group.id = UUID() try? viewContext.save() } } } catch { } } .toolbar { ToolbarItem { Button(action: addItems) { Label("Add Items", systemImage: "plus") } } } } } private func addItems() { let backgroundContext = persistenceController.container.newBackgroundContext() backgroundContext.perform { print("Add Items") var groups = try? backgroundContext.fetch(ItemGroup.fetchRequest()) for _ in 0..<10 { let newItem = Item(context: backgroundContext) newItem.id = UUID() newItem.group = groups![nextGroup % groups!.count] nextGroup += 1 try? backgroundContext.save() } } } } class PersistenceController: ObservableObject { let container: NSPersistentContainer init() { container = NSPersistentContainer(name: "TestCoreData") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) container.viewContext.automaticallyMergesChangesFromParent = true } } @main struct TestCoreDataApp: App { @StateObject private var persistenceController = PersistenceController() var body: some Scene { WindowGroup { ContentView() .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(persistenceController) } } }
问题:只要ItemGroup关联的Item变更并触发保存,即使id未变,publisher(for: \.id)也会触发回调,导致视图重渲染。
尝试2:自定义暂态字段触发通知
为ItemGroup实体添加两个int64类型的暂态字段pub和pub1,尝试手动控制变更通知:
视图代码
struct ItemGroupView: View { var itemGroup: ItemGroup var body: some View { let _ = Self._printChanges() Text("\(itemGroup.id!.uuidString) - \(itemGroup.count)") .onReceive(itemGroup.publisher(for: \.pub)) { _ in print("pub \(itemGroup.pub) \(itemGroup.id!.uuidString) changed")} } }
实体扩展代码
extension ItemGroup { override public func willChangeValue(forKey key: String) { if key == "pub1" { pub += 1 } super.willChangeValue(forKey: key) } }
问题:实现复杂且未精准匹配id变更的场景,未能有效解决过度渲染。
解决方案
方法1:用distinctUntilChanged过滤重复事件
利用Combine的distinctUntilChanged操作符,只在id值真正变化时触发回调,过滤掉同值重复事件:
struct ItemGroupView: View { var itemGroup: ItemGroup var body: some View { let _ = Self._printChanges() Text("\(itemGroup.id!.uuidString)") .onReceive(itemGroup.publisher(for: \.id).distinctUntilChanged()) { newId in print("id \(newId.uuidString) changed") } } }
该方法简单高效,直接基于现有Publisher处理,兼容iOS 13+,完美适配需求。
方法2:自定义CoreData实体的id变更通知
在ItemGroup中仅当id实际变更时发送自定义通知,视图监听该通知:
实体扩展
extension ItemGroup { private static let idChangedNotification = Notification.Name("ItemGroupIdChanged") override func willChangeValue(forKey key: String) { if key == "id" { let oldId = self.id super.willChangeValue(forKey: key) DispatchQueue.main.async { if self.id != oldId { NotificationCenter.default.post(name: Self.idChangedNotification, object: self) } } } else { super.willChangeValue(forKey: key) } } }
视图代码
struct ItemGroupView: View { var itemGroup: ItemGroup @State private var currentId: UUID? var body: some View { let _ = Self._printChanges() Text("\(itemGroup.id!.uuidString)") .onAppear { currentId = itemGroup.id NotificationCenter.default.addObserver( forName: ItemGroup.idChangedNotification, object: itemGroup, queue: .main ) { _ in if itemGroup.id != currentId { currentId = itemGroup.id print("id \(currentId!.uuidString) changed") } } } .onDisappear { NotificationCenter.default.removeObserver( self, name: ItemGroup.idChangedNotification, object: itemGroup ) } } }
此方式精准控制通知时机,适合需要更细粒度控制的场景。
方法3:优化CoreData批量操作
由于添加Item会触发ItemGroup的派生字段count变更,进而触发保存和通知,可通过批量操作减少通知次数:
修改addItems方法,批量添加后统一保存:
private func addItems() { let backgroundContext = persistenceController.container.newBackgroundContext() backgroundContext.perform { print("Add Items") guard let groups = try? backgroundContext.fetch(ItemGroup.fetchRequest()) else { return } for _ in 0..<10 { let newItem = Item(context: backgroundContext) newItem.id = UUID() newItem.group = groups[nextGroup % groups.count] nextGroup += 1 } // 批量添加完成后统一保存,减少变更通知次数 try? backgroundContext.save() } }
该方法能减少不必要的保存操作,间接降低视图重渲染次数。
内容的提问来源于stack exchange,提问作者John Cashew

