Search code examples
swiftcore-datadatabase-designnsmanagedobjecttransient

Is there a way to use a computed property with a parameter in Core Data?


This might be a weird question because I don't fully understand how transient and the new derived properties work in Core Data. So imagine I have a RegularClass entity, which stores any class that repeats over time. A class can repeat for example every third day or every one week. This is how it looks in the data model:

data model for RegularClass

(a RegularClass belongs to a Schedule entity, which in turn belongs to a Course entity)

Now, if our class repeats every third day, we store the number 3 in the frequency property, and a string "days" in unit property, which is then converted to an enum in Swift. A Schedule, which every RegularClass belongs to, has a startDate property.

To check if a class happens at a given date, I came up with nothing better than calculating the difference in specified unit between the startDate and the given date, then taking a remainder between the difference and frequency, and if it's 0, than it's the date in which a class can occur.

var differenceComponent: Calendar.Component {
    switch unitType {
    case .weeks:
        return .weekOfYear
    case .days:
        return .day
    }
}

func getDifferenceFromDateComponents(_ dateComponents: DateComponents) -> Int? {
    switch unitType {
    case .weeks:
        return dateComponents.weekOfYear
    case .days:
        return dateComponents.day
    }
}

func dateIsInActiveState(_ date: Date) -> Bool {
    if let startDate = schedule?.startDate {
        let comps = Calendar.current.dateComponents([differenceComponent], from: startDate, to: date)
        if let difference = getDifferenceFromDateComponents(comps) {
            let remainder = Int64(difference) % frequency // that is the key!
            return remainder == 0
        }
    }
    return false
}

func containsDate(_ date: Date) -> Bool {
    if dateIsInActiveState(date) {
        if unitType == .days {
            return true
        }

        let weekday = Calendar.current.component(.weekday, from: date)
        return (weekdays?.allObjects as? [Weekday])?.contains(where: { $0.number == weekday }) ?? false
    }

    return false
}

Now, the thing is that this code works perfectly for courses that I've already got from a fetch request. But is there a way to pass a date parameter in a NSPredicate to calculate this while request happens? Or do I have to fetch all the courses and then filter them out manually?


Solution

  • To solve this issue you could store your data as scalar types and then do simple arithmetic in your predicate. Rather than dates, use integers with a days-from-day-zero figure (or whatever minimum unit of time is necessary for these calculations). Store your repeat cycle as number-of-days.

    Then you can use the calculation ((searchDate - startDate) mod repeatCycle) == 0 in your predicate to find matching classes.

    As you have suggested, it might be sensible to denormalise your data for different search cases.