15

See also

Core Data Sort By Date With FetchedResultsController into sections

Is there a way to group objects from a CoreData entity by date and use them to create multiple sections on a UITableView control?

Essentially, I need something equivalent to the following pseudo-SQL, where my_group_criteria() groups all tests by day:

SELECT * FROM tests GROUP BY my_group_criteria(date)

As an example, the query would group the following rows into 3 separate sections:

[test1 | 2013-03-18 15.30.22]
[test2 | 2013-03-18 14.30.22]
[test3 | 2013-03-18 13.30.22]

[test4 | 2013-03-17 18.30.22]
[test5 | 2013-03-17 19.30.22]

[test6 | 2013-03-15 20.30.22]

As already answered in 2, NSPredicate is not for grouping entities so this may not be the way to go.

Given this, how do I go about creating multiple sections in a UITableView based on some criteria similar to what SQL GROUP BY would do?

I would like to avoid accessing the SQL-lite database directly if at all possible.

Related question 1: NSPredicate: filtering objects by day of NSDate property

Related question 2: NSPredicate something equivalent of SQL's GROUP BY

Community
  • 1
  • 1
bizz84
  • 1,964
  • 21
  • 34
  • 1
    Have a look at the [DateSectionTitles](http://developer.apple.com/library/ios/samplecode/DateSectionTitles/Introduction/Intro.html) sample project from Apple which does a very similar thing. – Martin R Feb 18 '13 at 21:55
  • As I understand it, the Apple example can get away with pre-defined header titles for the 12 months of the year. In my case though, the sections to be created are completely dynamic. Only days that have records in my entity should be displayed and some days may be missing altogether. I guess the approach could be adapted if I could associate sections directly with a field name with sectionNameKeyPath:. The problem is that I need a function of NSDate that extracts the yyyy-mm-dd info and discards the hh-mm-ss part. – bizz84 Feb 19 '13 at 11:49
  • 1
    The month names array in Apples example is used only to map the month number to a string. The sections are completely dynamic. I have added an answer which shows how you could proceed. Please let me know if you need more information. – Martin R Feb 19 '13 at 13:14

2 Answers2

14

You can modify the DateSectionTitles sample project from the Apple Developer Library according to you needs.

First, you have to modify the accessor function for the transient sectionIdentifier property to create a section identifier based on year+month+day (instead of year+month only):

- (NSString *)sectionIdentifier {

    // Create and cache the section identifier on demand.

    [self willAccessValueForKey:@"sectionIdentifier"];
    NSString *tmp = [self primitiveSectionIdentifier];
    [self didAccessValueForKey:@"sectionIdentifier"];

    if (!tmp) {
        /*
         Sections are organized by month and year. Create the section identifier
         as a string representing the number (year * 10000 + month * 100 + day);
         this way they will be correctly ordered chronologically regardless of
         the actual name of the month.
         */
        NSCalendar *calendar = [NSCalendar currentCalendar];

        NSDateComponents *components = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit)
                               fromDate:[self timeStamp]];
        tmp = [NSString stringWithFormat:@"%d", [components year] * 10000 + [components month] * 100  + [components day]];
        [self setPrimitiveSectionIdentifier:tmp];
    }
    return tmp;
}

Second, the titleForHeaderInSection delegate method must be changed, but the idea is the same: extract year, month and day from the section identifer, and create a header title string from that:

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {

    id <NSFetchedResultsSectionInfo> theSection = [[fetchedResultsController sections] objectAtIndex:section];

    NSInteger numericSection = [[theSection name] integerValue];
    NSInteger year = numericSection / 10000;
    NSInteger month = (numericSection / 100) % 100;
    NSInteger day = numericSection % 100;

    // Create header title YYYY-MM-DD (as an example):
    NSString *titleString = [NSString stringWithFormat:@"%d-%d-%d", year, month, day];
    return titleString;
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thank you for your solution, this fits my needs! Also I learnt a lot about primitives and transients in CoreData and while doing so I found that this has been asked before :D Updated on my question – bizz84 Feb 19 '13 at 16:03
  • I am not sure. I've plugged the above code in the sample code and I don't get the desired grouping. – p0lAris Oct 03 '14 at 07:13
  • FYI, there is a conversion typo, it should be `NSInteger year = numericSection / 1000;` .. not able to edit. – Ryan Romanchuk Jun 11 '15 at 21:43
  • @RyanRomanchuk: Why do you think so? The number represents YYYYMMDD, so you have to divide by 10000 to extract the year. – Martin R Jun 11 '15 at 22:58
0

Assuming the idea is to group the array of objects into an array of arrays of objects, the approach I would take is this, start by finding the possible values for date (or whatever you want to group by) and then actually sort it out.

NSArray *values = ...;
NSArray *sorting = [values valueForKey:@"@distinctUnionOfObject.propertyToGroupBy"];
NSUInteger sectionCount = [sorting count];
NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];
for (int i = 0; i < sectionCount; i++)
    [sections addObject:[NSMutableArray array]];

[values enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // Figure out the section for this object
    NSUInteger section = [sorting indexOfObject:obj.propertyToGroupBy];
    [[sections objectAtIndex:section] addObject:obj];
}];

// sections will contain as many sections as there were unique values
// each of those values is an array containing values that matched the criteria

This can likely be optimised, but from the top of my head, seems to fit the bill.

EDIT: After going through the question once again, the propertyToGroupBy would have to normalise the value, i.e remove time components of the date, as the grouping is done by unique values (i.e comparison is based on isEqual:)

Henri Normak
  • 4,695
  • 2
  • 23
  • 33
  • How would you go about normalising the value though? Following your example, I implemented a pre-processing method that creates a NSDictionary which will have all sections as sub-arrays. I can then use that directly on my UITableViewDataSource delegate methods instead of the NSFetchedResultsController. That works but may not be efficient when working with thousands of rows as it loses the optimisations included in NSFetchedResultsController. – bizz84 Feb 19 '13 at 11:52
  • Depends on the datatype the property refers to, if it's a date you can look at NSDateComponents in order to remove precision. Keep in mind that normalizing is rather expensive, so it might be a good idea to handle it once (in the setter of the property for example) – Henri Normak Feb 19 '13 at 14:40