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

如何仅在NSManagedObject指定字段变更时重渲染SwiftUI View?

问题:CoreData关联对象变更导致SwiftUI视图过度渲染

我的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 21:05:06