Search code examples
iosswiftcore-dataswiftuinsmanagedobject

How to remove duplicate dates (without time) from NSManagedObject in SwiftUI


I have this class, auto-generated from Xcode / Core Data for an Core Data entity called Activity.

@objc(Activity)
public class Activity: NSManagedObject {

}

extension Activity {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Activity> {
        return NSFetchRequest<Activity>(entityName: "Activity")
    }

    @NSManaged public var name: String
    @NSManaged public var date: Date

}

How can I get results with different dates (without time) only to not have duplicate dates for one view, but all dates (unfiltered) for another view?

I am not sure, if I need to modify my Activity Class and extend it with a custom function, or if I can apply a filter to the @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: true)]) var activites: FetchedResults<Activity>, or if I need to work on the requested result directly in the List(activities, id: \.self).

I need the flexibility of having both, all dates and the filtered dates. the filtered activites don't require the name and must not be unique. I just want a list with date (without time) strings. The unfiltered list requires both, name and date.

E.g.:

  • 2020-06-22
  • 2020-05-20
  • 2020-05-20
  • 2020-05-20
  • 2020-03-02

should be filtered to

  • 2020-06-22
  • 2020-05-20
  • 2020-03-02

Solution

  • The problem you describe is not really all that simple.

    The Date you are using is a time stamp defined in seconds from certain point in time. It is not related to any calendar. The strings that you have shown however are based on calendar and are an interpretation of a given time stamp.

    So when using a calendar (including time zone) the interpretation of a single date may be different for two different users mostly depending on the zone.

    The database I believe stores the Date object as a 64-bit floating value. So to get filtered dates you are basically asking to get all unique values where these values are first rounded by some absurd "calendar" algorithm. So good luck with that.

    The easiest is to do this in memory. You can fetch all your entries and then use a dictionary to group your items:

    func sortEntriesByDays(_ activities: [Activity]) -> [Date: [Activity]] {
        let calendar = Calendar.autoupdatingCurrent
        
        var sortedItems: [Date: [Activity]] = [Date: [Activity]]()
        
        activities.forEach { activity in
            let dayStartDate = calendar.date(from: calendar.dateComponents([.year, .month, .day], from: activity.date))!
            
            var activities: [Activity] = sortedItems[dayStartDate] ?? [Activity]()
            activities.append(activity)
            
            sortedItems[dayStartDate] = activities
        }
        
        return sortedItems
    }
    

    You can get all unique dates from result of this methods by simply calling .keys on it.

    This is probably the easiest and safest solution. The only problem is when a lot of entries are in your database you may want to not load all (or very many) of them just to display a few. In those cases a fetch result controller is very useful but to use it you may need to change your database.

    One approach is to have 2 dates stored in your Activity. One is the actual date that you already have stored and the other one is a beginning of the date it belongs to. You could simply create a setDate method which could set both dates; the second one simply uses the code: calendar.date(from: calendar.dateComponents([.year, .month, .day], from: activity.date))!.

    A very similar approach is to just add a string using format yyyy-MM-dd as you described it in question. It does not change much from previous idea.

    Then another approach is to create another entity Day which holds many-to-one relations to your activity objects. So each activity should have a reference to exactly one day. So when an activity should be inserted into database it should try and find a corresponding day object and connect with it. If there is no such Day object then it needs to create one. When object is modified it needs to recheck if it belongs to same day. When activity is removed from Day object you would also need to check if there are any other activities left and remove the Day object from database when it has no activities.

    But whichever of the 3 options you use you may have a problem when user changes time zone (possibly from traveling). When that happens the code calendar.date(from: calendar.dateComponents([.year, .month, .day], from: activity.date))! will produce different result and it is possible that activity should now belong to a different Day object. You could detect that change and restructure your database...