0

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.

11
  • 1
    Unrelated but do not name a custom object Data, it could interfere with Foundation Data. Commented Jun 22, 2024 at 17:57
  • 1
    You could make a child view that only takes the properties as inputs that you care about changing. Commented Jun 22, 2024 at 19:23
  • 1
    You could make a reducer that acts as go-between between an actual MyData and CellView. The reducer type would consist of only the relevant properties. Commented Jun 22, 2024 at 19:38
  • 1
    Don’t use ObservableObjects use Observable or struct. If you switch the view will only redraw when something that it is using changes. Equatable for a View is never the right solution. Commented Jun 22, 2024 at 19:45
  • 1
    If MyData is a CoreData object there is no way to tell. Observable objects only have 1 publisher for all variables Commented Jun 22, 2024 at 22:05

1 Answer 1

0

First, try setting the @Published var less frequently, that way objectWillChange will be sent less often, and thus body called less often.

Then if you have multiple @Published vars and want to tighten up body invalidation, simply make a custom View that only has the attribute you want, body will only be called when the value of the attribute changes, e.g.

struct ParentView: View { @ObservedObject var object: MyObject var body: some View { // body called when any @Published property of object changed AttributeView(attribute: object.attribute) } } struct AttributeView: View { let attribute: String var body: some View { // body only called when attribute has changed Text(attribute) } } 

FYI SwiftUI doesn't render anything, it just diffs these View data structs after it detects a relevant change and uses the difference to init/update/deinit UIKit objects. So the smaller your structs, the faster it can diff. If there is no difference, then there is no need to update UIKit objects.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you, but this does not help. The problem is that body is called far too many times, even though it's already very cheap.
Try setting the @Published var less frequently

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.