Home > Net >  Use .fetchBatchSize with @FetchRequest in SwifUI
Use .fetchBatchSize with @FetchRequest in SwifUI

Time:02-23

I've been trying to understand and figure it out for a few days now, but still couldn't come up with solution.

For example, there is a core data entity with 1 mil rows, and I want to display all the record in a view with a "load as you scroll" in order increase the perfomance, as loading 1 mil rows on load would be slow and wasteful.

From the documentaion basically .fetchBatchSize will load 'batches' of data only when it's needed, which is perfect solution for a problem. A user scroll the list, and SwiftUI load data by batches when required.

Here is what I have (simplified):

ContextView.swift

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

The problem: @FetchRequest loads all the records at the same time without batching and/or it does work but retrieves all the batches at the same time and defeating the whole purpose of batch retrieving.

I tried actually loading 1 mil rows and takes a lot of time to show the view (as it is retrieving and preparing all 1 mil rows of data). If '.fetchBatchSize' worked it will load only the first 5 and will load slowly as the list scrolls.

*Note: I know there is .fetchLimit & .fetchOffset, but it would require to implement a separate logic *

CodePudding user response:

You can't use fetchBatchSize because the List needs all of the objectIDs that SwiftUI diffs to detect inserts, removes & moves. That's why it loads all the batches if you set a batch size which is much slower than no batching.

You can however disable includesPropertyValues to prevent Core Data loading all of the data into its row cache.

With it disabled the SQL query becomes:

SELECT 0, t0.Z_PK FROM ZITEM t0

Instead of the default of enabled where it fetches all fields:

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

To see the queries use launch argument -com.apple.CoreData.SQLDebug 4 (higher the number the more log output).

You can also take advantage of List's lazy behaviour by moving the $0.name into a body of a child View so that the Core Data only hits the database to read the data and fire the fault of the object when it scrolls on to the screen.

These two improvements may give a large enough increase in performance and would look like this:

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)
...
  • Related