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

如何解决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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 07:34:54