How to Properly Group a List Fetched from Coredata by Date

How to properly group a list fetched from CoreData by date?

You may try the following, It should work in your situation.

  @Environment(\.managedObjectContext) var moc
@State private var date = Date()
@FetchRequest(
entity: Todo.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Todo.date, ascending: true)
]
) var todos: FetchedResults<Todo>

var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}

func update(_ result : FetchedResults<Todo>)-> [[Todo]]{
return Dictionary(grouping: result){ (element : Todo) in
dateFormatter.string(from: element.date!)
}.values.map{$0}
}

var body: some View {
VStack {
List {
ForEach(update(todos), id: \.self) { (section: [Todo]) in
Section(header: Text( self.dateFormatter.string(from: section[0].date!))) {
ForEach(section, id: \.self) { todo in
HStack {
Text(todo.title ?? "")
Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
}
}
}
}.id(todos.count)
}
Form {
DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
Text("Datum")
}
}
Button(action: {
let newTodo = Todo(context: self.moc)
newTodo.title = String(Int.random(in: 0 ..< 100))
newTodo.date = self.date
newTodo.id = UUID()
try? self.moc.save()
}, label: {
Text("Add new todo")
})
}
}

How to group core data items by date in SwiftUI?

I'm just getting into SwiftUI myself, so this might be a misunderstanding, but I think the issue is that the update function is unstable, in the sense that it does not guarantee to return the groups in the same order each time. SwiftUI consequently gets confused when new items are added. I found that the errors were avoided by specifically sorting the array:

func update(_ result : FetchedResults<Todo>)-> [[Todo]]{
return Dictionary(grouping: result){ (element : Todo) in
dateFormatter.string(from: element.date!)
}.values.sorted() { $0[0].date! < $1[0].date! }
}

Grouping CoreData by Date() in SwiftUI List as sections

You can make your own sectionIdentifier in your entity extension that works with @SectionedFetchRequest

The return variable just has to return something your range has in common for it to work.

extension Todo{
///Return the string representation of the relative date for the supported range (year, month, and day)
///The ranges include today, tomorrow, overdue, within 7 days, and future
@objc
var dueDateRelative: String{
var result = ""
if self.dueDate != nil{
//Order matters here so you can avoid overlapping
if Calendar.current.isDateInToday(self.dueDate!){
result = "today"//You can localize here if you support it
}else if Calendar.current.isDateInTomorrow(self.dueDate!){
result = "tomorrow"//You can localize here if you support it
}else if Calendar.current.dateComponents([.day], from: Date(), to: self.dueDate!).day ?? 8 <= 0{
result = "overdue"//You can localize here if you support it
}else if Calendar.current.dateComponents([.day], from: Date(), to: self.dueDate!).day ?? 8 <= 7{
result = "within 7 days"//You can localize here if you support it
}else{
result = "future"//You can localize here if you support it
}
}else{
result = "unknown"//You can localize here if you support it
}
return result
}
}

Then use it with your @SectionedFetchRequest like this

@SectionedFetchRequest(entity: Todo.entity(), sectionIdentifier: \.dueDateRelative, sortDescriptors: [NSSortDescriptor(keyPath: \Todo.dueDate, ascending: true)], predicate: nil, animation: Animation.linear)
var sections: SectionedFetchResults<String, Todo>

Look at this question too

You can use Date too but you have to pick a date to be the section header. In this scenario you can use the upperBound date of your range, just the date not the time because the time could create other sections if they don't match.

extension Todo{
///Return the upperboud date of the available range (year, month, and day)
///The ranges include today, tomorrow, overdue, within 7 days, and future
@objc
var upperBoundDueDate: Date{
//The return value has to be identical for the sections to match
//So instead of returning the available date you return a date with only year, month and day
//We will comprare the result to today's components
let todayComp = Calendar.current.dateComponents([.year,.month,.day], from: Date())
var today = Calendar.current.date(from: todayComp) ?? Date()
if self.dueDate != nil{
//Use the methods available in calendar to identify the ranges
//Today
if Calendar.current.isDateInToday(self.dueDate!){
//The result variable is already setup to today
//result = result
}else if Calendar.current.isDateInTomorrow(self.dueDate!){
//Add one day to today
today = Calendar.current.date(byAdding: .day, value: 1, to: today)!
}else if Calendar.current.dateComponents([.day], from: today, to: self.dueDate!).day ?? 8 <= 0{
//Reduce one day to today to return yesterday
today = Calendar.current.date(byAdding: .day, value: -1, to: today)!
}else if Calendar.current.dateComponents([.day], from: today, to: self.dueDate!).day ?? 8 <= 7{
//Return the date in 7 days
today = Calendar.current.date(byAdding: .day, value: 7, to: today)!
}else{
today = Date.distantFuture
}
}else{
//This is something that needs to be handled. What do you want as the default if the date is nil
today = Date.distantPast
}
return today
}
}

And then the request will look like this...

@SectionedFetchRequest(entity: Todo.entity(), sectionIdentifier: \.upperBoundDueDate, sortDescriptors: [NSSortDescriptor(keyPath: \Todo.dueDate, ascending: true)], predicate: nil, animation: Animation.linear)
var sections: SectionedFetchResults<Date, Todo>

Based on the info you have provided you can test this code by pasting the extensions I have provided into a .swift file in your project and replacing your fetch request with the one you want to use

Grouping FechedResults by Month/Year to display a swiftui list with sections

You can sort the the fetched results if you use a different key in the temporary dictionary that can be properly sorted. So for the DateFormatter used in the function I set the format to "yyyy-MM" instead.

Note that I start by sorting the input to the function but this step is not needed if the fetched result is already sorted on date which I recommend.

func group(_ result : FetchedResults<LogEntry>) -> [[LogEntry]] {
let sorted = result.sorted { $0.date < $1.date }

return Dictionary(grouping: sorted) { (element : LogEntry) in
dictionaryDateFormatter.string(from: element.date as Date)
}.sorted { $0.key < $1.key }.map(\.value)
}

How to fetch Core Data data from a specific date?

You get the last Monday with the help of Calendar searching backwards for the first occurrence of weekday = 2

let currentDate = Date()
let mondayComponent = DateComponents(weekday: 2)
let lastMonday = Calendar.current.nextDate(after: currentDate,
matching: mondayComponent,
matchingPolicy: .nextTime,
repeatedTimePolicy: .first,
direction: .backward)!

Then create a predicate

let predicate = NSPredicate(format: date >= %@, lastMonday as NSDate)

and apply this predicate to your fetch request.


Side note:

YYYY is wrong. In a standard calendar use always yyyy

Core Data Group By Year and Sort By Date

  1. Create an NSFetchRequest. Add a sort descriptor for the date column. That will get your rows in order.
  2. Execute the fetch request on your managed object context, getting back your results
  3. Break each date into components (using something like this.
  4. Iterate, build arrays, whatever you need to do to massage your results into the format you want.

1 will look something like this:

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entry"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"flightDate" ascending:NO];
fetchRequest.sortDescriptors = @[sortDescriptor];

Group by one property while retrieving more with core data

With a proviso that I think this will work; I leave it to you to test it to death:

You cannot add any other attributes to propertiesToFetch, beyond those in the propertiesToGroupBy, but it seems you can include the objectID in the properties to fetch. To do so, build an NSExpression and associated NSExpressionDescription for the "evaluated object":

NSExpression *selfExp = [NSExpression expressionForEvaluatedObject];
NSExpressionDescription *selfED = [[NSExpressionDescription alloc] init];
selfED.name = @"objID";
selfED.expression = selfExp;
selfED.expressionResultType = NSObjectIDAttributeType;

Now define another expression/description to get the max(date):

NSExpression *maxDate = [NSExpression expressionForKeyPath:@"date"];
NSExpression *indexExp = [NSExpression expressionForFunction:@"max:" arguments:@[maxDate]];
NSExpressionDescription *maxED = [[NSExpressionDescription alloc] init];
maxED.name = @"maxDate";
maxED.expression = indexExp;
maxED.expressionResultType = NSDateAttributeType;

Then include these two expression descriptions in the list of properties to fetch:

[request setPropertiesToFetch:@[@"phone", maxED, selfED]];
[request setPropertiesToGroupBy:@[@"phone"]];

When you run the fetch, each item in the resulting array will have a key "objID" containing the objectID for the relevant object. You can unpack that, to access the name, phone, etc, with something along these lines:

NSArray *results = [self.context executeFetchRequest:request error:&error];
for (NSDictionary *dict in results) {
NSDate *maxDate = dict[@"maxDate"];
NSString *phone = dict[@"phone"];
NSManagedObjectID *objID = [dict valueForKey:@"objID"];
NSManagedObject *object = [self.context objectWithID:objID];
NSString *name = [object valueForKey:@"name"];
}

One particular aspect I am unsure of is how this will behave if two rows have exactly the same date.



Related Topics



Leave a reply



Submit