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.