在 SwiftUI 中将 .fetch 批量大小与 @FetchRequest 结合使用

Use .fetchBatchSize with @FetchRequest in SwifUI

这几天我一直在努力理解和弄明白,但仍然想不出解决办法。

例如,有一个包含 100 万行的核心数据实体,我想在一个视图中显示所有记录,并带有“滚动时加载”以提高性能,因为加载 100 万行加载会很慢而且很浪费。

根据文档,基本上 .fetchBatchSize 只会在需要时加载 'batches' 数据,这是解决问题的完美方法。用户滚动列表,SwiftUI 在需要时批量加载数据。

这是我的(简体):

ContextView.swift

struct ContentView: View {
    @FetchRequest private var items: FetchedResults<Item>
    
    init() {
        _items = FetchRequest<Item>(fetchRequest: request())
    }
    
    var body: some View {
        List(items) {
            Text([=10=].name)
        }
    }
    
    func request() -> NSFetchRequest<Item> {
        let request = Item.fetchRequest()
        request.sortDescriptors = []
        request.fetchBatchSize = 5
        
        return request
    }
}

问题:@FetchRequest 同时加载所有记录而不进行批处理 and/or 它确实有效但同时检索所有批次并击败批量检索的全部目的。

我尝试实际加载 100 万行,但需要花费大量时间来显示视图(因为它正在检索和准备所有 100 万行数据)。如果“.fetchBatchSize”有效,它将仅加载前 5 个,并且会随着列表滚动而缓慢加载。

*注意:我知道有 .fetchLimit & .fetchOffset,但它需要实现一个单独的逻辑 *

您不能使用 fetchBatchSize,因为 List 需要 SwiftUI diff 检测插入、删除和移动的所有 objectID。这就是为什么如果您设置的批处理大小比没有批处理慢得多,它会加载所有批处理。

但是您可以禁用 includesPropertyValues 以防止 Core Data 将所有数据加载到其行缓存中。

禁用后 SQL 查询变为:

SELECT 0, t0.Z_PK FROM ZITEM t0

而不是在获取所有字段的地方默认启用:

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZTIMESTAMP FROM ZITEM t0

要查看查询,请使用启动参数 -com.apple.CoreData.SQLDebug 4(数字越大,日志输出越多)。

您还可以通过将 [=20=].name 移动到子视图的主体中来利用 List 的惰性行为,以便核心数据仅访问数据库以读取数据并在它滚动到屏幕上,您会在日志中看到:

CoreData: details: SQLite: EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTIMESTAMP, t0.ZTITLE FROM ZITEM t0 WHERE  t0.Z_PK = ? 
     2 0 0 SEARCH t0 USING INTEGER PRIMARY KEY (rowid=?)
CoreData: annotation: fault fulfilled from database for : 0xa17723cef738c1a0 <x-coredata://FA6F121D-805F-4558-AAAC-F5F60A117256/Item/p9778> with row values: <NSSQLRow: 0x600001745b60>{Item 1-9778-1 timestamp=2022-02-24 13:58:39 +0000 title=NULL and to-manys=0x0}

这些改进可能会带来足够大的性能提升,看起来像这样:

extension Item {
    static func myFetchRequest() -> NSFetchRequest<Item> {
        let fr = Self.fetchRequest()
        fr.includesPropertyValues = false
        fr.sortDescriptors = [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)]
        return fr
    }
}

struct MyView: View {
    @ObservedObject var item: Item
    
    var body: some View {
        Text(item.timestamp!, formatter: itemFormatter)
    }
}

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        fetchRequest: Item.myFetchRequest(),
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    MyView(item: item)
...

如果您使用@SectionedFetchRequest,那么您将需要获取分段中使用的字段,这可以通过保留 includesPropertyValues 其默认值 true 并将 propertiesToFetch 设置为数组来完成属性 用于分段的名称。

例如

extension Item {
    static func myFetchRequest() -> NSFetchRequest<Item> {
        let fr = Self.fetchRequest()
        fr.propertiesToFetch = ["title"]
        fr.sortDescriptors = [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)]
        return fr
    }
}

初始查询结果:

CoreData: sql: SELECT 1, t0.Z_PK, t0.ZTITLE FROM ZITEM t0 ORDER BY t0.ZTIMESTAMP

如您所见,select timestamp 并非如此。当列表滚动并访问对象时,会执行其他查询以检索所有字段。