How to enumerate a fetch request to handle lots of data efficiently
How to enumerate a fetch request to handle lots of data efficiently êŽë š
Updated for Xcode 15
SwiftDataâs model context has a dedicated enumerate()
method that is designed to traverse large amounts of data efficiently. Whether itâs effective or not is down to you to judge, but I can at least show you how it works.
Iâll explain the drawbacks more in a moment, but first letâs look at some examples. If you worked at a large university with thousands of students and wanted to process them somehow â for example if you wanted to loop over everyone to calculate the total number of test scores by all students, as well as how many results were regular passes and how many were passes with distinction â then you might write code like this:1
let descriptor = FetchDescriptor<Student>()
var totalResults = 0
var totalDistinctions = 0
var totalPasses = 0
do {
try modelContext.enumerate(descriptor) { student in
totalResults += student.scores.count
totalDistinctions += student.scores.filter { $0 >= 85 }.count
totalPasses += student.scores.filter { $0 >= 70 && $0 < 85 }.count
}
} catch {
print("Unable to calculate student results.")
}
print("Total test results: \(totalResults)")
print("Distinctions: \(totalDistinctions)")
print("Passes: \(totalPasses)")
That loops over batches of students, adding however many tests they have taken to the total, then filtering their results and adding to either totalDistinctions
or totalPasses
.
The default batch size is 5000 objects, so SwiftData will load the first 5000 students, process them in the batch, then load the next 5000, and so on.
You can adjust the batch size using the batchSize
parameter. For example, if I knew that each of my Student
objects was quite large, for example if they had image data stored, it would be smart to reduce the batch size to 1000 or less so that fewer objects are loaded at a time:
try modelContext.enumerate(descriptor, batchSize: 1000) { student in
This is a trade-off: using a lower batch size ought to save memory, but will increase the amount of disk I/O. Similarly, using a larger batch size will reduce disk I/O but increase memory.
All that probably sounds very welcome, but in practice things arenât so straightforward. Yes, adjusting the batch size ought to let us trade memory for I/O to get performance that fits our app, but at the time of writing SwiftData seems to keep all the objects in memory even after they have been processed, so you get huge memory bloat even with a reduced batch size.
Apple bills enumerate()
as a method that âencapsulates platform best practices for traversing lots of data,â but please double check it to make sure it actually works well in your project.