My app has an ItemGroupView which is rendered many more times than needed when I import a large dataset into the app. This causes hangs in my app. I know ItemGroupView, below, is the view that is rendered too frequently from using Instruments. Making itemGroup an @Observable might solve this problem, but I need to support iOS 16. The problem is that .onReceive(itemGroup.publisher(for: \.id)) is called any time an ItemGroup is save()ed, regardless of whether id changed. I have such a large number of ItemGroupViews that rerendering them all is expensive even though each individual rerender is super cheap.
How can my view receive an event only when id has changed?
Attempt 1:
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) } } } The app has CoreData model with 2 Entities: Item and ItemGroup. Both have a single UUID field called id. They have a 1 to many relationship. ItemGroup has a count derived fieldwith derivationitem.@count`.
Attempt 2:
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) } }
In this attempt I augmented the app's CoreData model ItemGroup entity with 2 transient int64 fields named pub and pub.
Data, it could interfere with FoundationData.