Querying SwiftData with a date-based Predicate
I was working on a project where I needed to query items from swift data that occurred on a specific date. When dealing with dates, my first instinct is usually to reach for the Calendar object to do all the heavy lifting for me. This lead me to have something like:
private func fixturesHappeningToday() -> [LocalFixture] {
let predicate = #Predicate<LocalFixture> { item in
Calendar.current.isDateInToday(item.date)
}
let descriptor = FetchDescriptor<LocalFixture>(predicate: predicate)
let results = try? modelContext.fetch(descriptor)
return results ?? []
}
// Compiler error:
// The isDateInToday(_:) function is not supported in this predicate
That didn’t work out, and it kind of makes sense since the Predicate needs to be converted to SQL eventually. So instead I pivoted and tried to just format the values and compare them.
let predicate = #Predicate<LocalFixture> { item in
item.date.formatted(date: .numeric, time: .omitted) == Date.now.formatted(date: .numeric, time: .omitted)
}
// Compiler error:
// The formatted(date:time:) function is not supported in this predicate
Yep, pretty much the same compiler error. I probably should have guessed that the formatted function would have the same limitation, but it was worth a shot.
At one point I considered storing pieces of the date values, but I didn’t like the idea of having duplicate data. The next idea was to create a computed, transient property. This actually compiled! Hooray.
var dayMonthYear: String {
date.formatted(.dateTime.day().month().year())
}
let predicate = #Predicate<LocalFixture> { item in
fixture.dayMonthYear == match
}
And then promptly crashed when the app was run.
// Fatal error: Couldn't find \LocalFixture.dayMonthYear on LocalFixture with fields dayMonthYear
At this point, I thought perhaps I was just overthinking all of this and should just compare the dates directly.
let targetDate = Date.now
let predicate = #Predicate<LocalFixture> { item in
item.date == targetDate // Can't use Date.now directly in #Predicate
}
No crash this time, but I didn’t get any results back. After a few minutes of head scratching it dawned on me that this was of course comparing the full date value, including the time. After a stretch, and a quick walk away from my desk, I finally hit on an idea that worked. Utilize the Calendar to create start and end dates, and then do simple compares against those directly.
let start = Calendar.current.startOfDay(for: targetDate)
let end = Calendar.current.date(byAdding: .day, value: 1, to: start)!
let predicate = #Predicate<LocalFixture> { item in
item.date > start && item.date < end
}
We can’t really perform any operations on item.date, but we CAN do some pre-calculations to make it easier to compare against. Using theses Calendar methods we can safely create a range from midnight to midnight, fully encapsulating our target date.