如何解决macOS SwiftUI中Core Data驱动的ScrollView无响应问题?
我是SwiftUI新手,在macOS平台开发。我有一个ChatView用于显示ChatBubble列表,所有持久化数据采用Core Data管理。
在ChatView中,我按如下方式初始化FetchRequest:
_messages = FetchRequest(entity: Message.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Message.date, ascending: false)], predicate: NSPredicate(format: "%K == %@", #keyPath(Message.belongs_to_convo), ce as CVarArg), animation: .none)
其中messages是通过@FetchRequest属性包装器声明的FetchedResults<Message>。
最后一个ChatBubble显示的是从服务器流式传输的数据,服务器会直接将数据写入Message(Core Data托管实体)。问题在于,由于短时间内有大量更新,数据添加期间(约10秒)UI变得无响应(无法滚动)。
我通过Instruments未能精确定位卡顿原因,但在ChatBubble上使用Self._printChanges()后发现,每次Core Data写入数据时,所有ChatBubble都会重绘(即使未发生变化的也会)。
我尝试的两种修复方法均失败:
- 将写入操作移至后台上下文(使用
persistentStorage.newBackgroundContext()和backgroundContext.perform { }),虽释放了主线程资源,但屏幕无法实时更新数据。 - 将FetchRequest拆分为两个:一个获取不常变化的消息(除最后一条外的所有消息),另一个获取频繁更新的最后一条消息。我将FetchRequest的谓词替换为
%K == %@ AND is_finished == true,能获取预期消息,但仍无法解决问题——每当写入结果外的消息时,所有视图仍会像FetchedResults更新一样重绘。
补充说明:ChatBubble的绘制成本较高,因为每个气泡可能包含高级格式,需要大量处理。
1. 让ChatBubble仅在自身数据变化时重绘
SwiftUI视图默认会随父视图状态变化重绘,给ChatBubble实现Equatable协议,只对比影响UI展示的关键属性,这样只有当前气泡绑定的Message实际变更时才会触发重绘:
struct ChatBubble: View, Equatable { let message: Message static func == (lhs: ChatBubble, rhs: ChatBubble) -> Bool { // 只比较影响UI的核心属性 lhs.message.id == rhs.message.id && lhs.message.content == rhs.message.content && lhs.message.is_finished == rhs.message.is_finished } var body: some View { // 你的气泡内容实现 } }
在列表中使用时加上.equatable()修饰符:
List(messages) { message in ChatBubble(message: message) .equatable() }
2. 正确配置Core Data后台上下文的合并策略
之前后台写入后UI不更新,是因为主上下文没有设置自动合并后台更改。在Core Data栈初始化时添加以下配置:
let persistentContainer = NSPersistentContainer(name: "YourModelName") persistentContainer.loadPersistentStores { description, error in // 处理加载错误 // 开启主上下文自动合并后台上下文的更改 persistentContainer.viewContext.automaticallyMergesChangesFromParent = true // 设置合并策略(可根据需求调整,这里优先保留主上下文的变更) persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy } // 后台写入的正确用法 let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.perform { // 获取或创建目标Message实体 let targetMessage = // 你的实体查询逻辑 targetMessage.content = newStreamingContent targetMessage.is_finished = false do { try backgroundContext.save() } catch { // 处理保存错误 } }
这样后台写入的变更会自动同步到主上下文,@FetchRequest能实时感知变化,同时写入操作完全在后台执行,不会阻塞主线程。
3. 拆分FetchRequest并隔离频繁更新的视图
之前拆分FetchRequest无效,是因为父视图仍会因FetchedResults整体更新重绘。可以把频繁更新的消息单独放在子视图中,各自监听对应的FetchRequest:
struct ChatView: View { let conversation: Conversation // 历史消息:仅获取已完成的 @FetchRequest var historicalMessages: FetchedResults<Message> // 正在更新的消息:获取未完成的 @FetchRequest var activeMessage: FetchedResults<Message> init(conversation: Conversation) { self.conversation = conversation _historicalMessages = FetchRequest( entity: Message.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Message.date, ascending: false)], predicate: NSPredicate(format: "%K == %@ AND is_finished == true", #keyPath(Message.belongs_to_convo), conversation as CVarArg), animation: .none ) _activeMessage = FetchRequest( entity: Message.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Message.date, ascending: false)], predicate: NSPredicate(format: "%K == %@ AND is_finished == false", #keyPath(Message.belongs_to_convo), conversation as CVarArg), animation: .none ) } var body: some View { List { ForEach(historicalMessages) { message in ChatBubble(message: message) .equatable() } // 仅渲染正在更新的消息 if let message = activeMessage.first { ChatBubble(message: message) .equatable() } } } }
这样只有正在更新的ChatBubble会随流式数据重绘,历史消息列表不受影响。
4. 优化ChatBubble的绘制性能
由于气泡绘制成本高,可通过缓存减少重复计算:
- 将高级格式内容缓存成
NSImage,避免每次重绘都重新渲染富文本:
struct ChatBubble: View, Equatable { let message: Message @State private var renderedContent: NSImage? var body: some View { if let image = renderedContent { Image(nsImage: image) } else { Text(message.content) .hidden() .onAppear { renderedContent = renderMessageToImage(message) } .onChange(of: message.content) { _ in renderedContent = renderMessageToImage(message) } } } private func renderMessageToImage(_ message: Message) -> NSImage { // 实现将富文本内容转换为NSImage的逻辑 } }
- 若不需要List的原生交互,可使用
LazyVStack替代,或给List添加.listStyle(.plain)减少系统渲染开销。
内容的提问来源于stack exchange,提问作者THC

