Brent Priddy
Brent Priddy

Reputation: 3847

NSFetchedResultsController v.s. UILocalizedIndexedCollation

I am trying to use a FRC with mixed language data and want to have a section index.

It seems like from the documentation you should be able to override the FRC's

- (NSString *)sectionIndexTitleForSectionName:(NSString *)sectionName
- (NSArray *)sectionIndexTitles

and then use the UILocalizedIndexedCollation to have a localized index and sections. But sadly this does not work and is not what is intended to be used :(

Has anyone been able to use a FRC with UILocalizedIndexedCollation or are we forced to use the manual sorting method mentioned in the example UITableView + UILocalizedIndexedCollation example (example code included where I got this working).

Using the following properties

@property (nonatomic, assign) UILocalizedIndexedCollation *collation;
@property (nonatomic, assign) NSMutableArray *collatedSections;

and the code:

- (UILocalizedIndexedCollation *)collation
{
    if(collation == nil)
    {
        collation = [UILocalizedIndexedCollation currentCollation];
    }

    return collation;
}

- (NSArray *)collatedSections
{
    if(_collatedSections == nil)
    {
        int sectionTitlesCount = [[self.collation sectionTitles] count];

        NSMutableArray *newSectionsArray = [[NSMutableArray alloc] initWithCapacity:sectionTitlesCount];
        collatedSections = newSectionsArray;
        NSMutableArray *sectionsCArray[sectionTitlesCount];

        // Set up the sections array: elements are mutable arrays that will contain the time zones for that section.
        for(int index = 0; index < sectionTitlesCount; index++) 
        {
            NSMutableArray *array = [[NSMutableArray alloc] init];
            [newSectionsArray addObject:array];
            sectionsCArray[index] = array;
            [array release];
        }


        for(NSManagedObject *call in self.fetchedResultsController.fetchedObjects)
        {
            int section = [collation sectionForObject:call collationStringSelector:NSSelectorFromString(name)];
            [sectionsCArray[section] addObject:call];
        }

        NSArray *sortDescriptors = self.fetchedResultsController.fetchRequest.sortDescriptors;
        for(int index = 0; index < sectionTitlesCount; index++) 
        {
            [newSectionsArray replaceObjectAtIndex:index withObject:[sectionsCArray[index] sortedArrayUsingDescriptors:sortDescriptors]];
        }
    }
    return [[collatedSections retain] autorelease];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
{
    // The number of sections is the same as the number of titles in the collation.
    return [[self.collation sectionTitles] count];
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
{
    // The number of time zones in the section is the count of the array associated with the section in the sections array.
    return [[self.collatedSections objectAtIndex:section] count];
}


- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section 
{
    if([[self.collatedSections objectAtIndex:section] count])
        return [[self.collation sectionTitles] objectAtIndex:section];
    return nil;
}


- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return [self.collation sectionIndexTitles];
}


- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
    return [self.collation sectionForSectionIndexTitleAtIndex:index];
}

I would love to still be able to use the FRCDelegate protocol to be notified of updates. It seems like there is no good way making these two objects work together nicely.

Upvotes: 14

Views: 7118

Answers (4)

Alvin
Alvin

Reputation: 31

I found a easy way to solve this!

Just replace "#" to "^" in your core data so that the sections for your tableview will be "A-Z^". While unicode of '#' is smaller than 'A', '^''s is just the opposite. So it's not difficult for you to predict '^' will follow Z in your sections.

Then, you should replace your fetched results controller's sections. just by this couple of lines code:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{

    NSMutableArray *array = [[NSMutableArray alloc] initWithArray:[self.frc sectionIndexTitles]];

    // If "^" is in the section, replace it to "#"
    if ( [[array lastObject] isEqualToString:@"^"])
    {
        [array setObject:@"#" atIndexedSubscript:[array count]-1];
        return array;
    }
    // If "#" is not in the section
    return [self.frc sectionIndexTitles];
}

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title
               atIndex:(NSInteger)index
{
    if ([title isEqualToString:@"#"]) {
        return [self.frc sectionForSectionIndexTitle:@"^" atIndex:index];
    }
    return [self.frc sectionForSectionIndexTitle:title atIndex:index];
}

-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    if ([[[self.frc sectionIndexTitles] objectAtIndex:section] isEqualToString:@"^"]) {
        return @"#";
    }
    return [[self.frc sectionIndexTitles] objectAtIndex:section];
}

Upvotes: 2

dimanitm
dimanitm

Reputation: 101

Facing the same problem recently makes me searching web (stackoverflow firstly) for appropriate solution to make NSFetchedResultsController (FRC) and UILocalizedIndexedCollation (LIC) work together. Most find solutions wasn't good enough to fulfil all requirement. It is important to mention that we cannot use LIC to sort fetched objects in the way it needs course we will have huge performance lose and FRC wouldn't give use all advantage.

So, here is the problem in general:

1) We have DB with some kind of data that we want to fetch and display using FRC in a list (UITableView) with indexes (similar to Contacts.app). We need to pass object value key so FRC can make sort decision.

2) Even if we will add special field to our CoreData models for sections sorting and use FRC's section index titles we will not achieve desired result, course FRC only gives found indexes, but not complete alphabet. In bad addition to that we'll face problem with incorrect indexes displaying (not really sure why's that so, maybe some bug in FRC). In case of Russian alphabet, for example, there will be totally blank or "strange" symbols ($, ?, ', …).

3) If we will try to use LIC to display nice localized indexes we will face the problem of mapping data-based sections in FRC to complete localized alphabet "sections" in LIC.

4) After we decided to use LIC and somehow solve problem 3) we will notice that LIC will place "#" section to bottom (i.e. highest section index) but FRC will place "#"-like objects to top (i.e. lowest section index - 0). So will have complete sections displacement.

Taking all that into a count I decided to "trick" FRC without any big "hacking" but make it sort data in the way I need (move all objects that are from "#"-like section to bottom of list).

Here is the solution that I came to:

I add extension method to my NSManagedObject instance to prepare sort name that we will use in sort descriptor and section key path for FRC setup. No special moves needed except those one that will be described below.

Problem 4) occurs due to FRC's sorting algos (low-level SQL) that can be modified slightly: only by applying sort descriptors that are more your-data-dependant, predicates and using fixed predefined comparators that don't solve the problem.

I noticed that FRC decides that "#" symbol is lower that any alphabet symbol opposite to LIC where "#" is highest.

FRC's logic is pretty straightforward because "#" symbol in UTF-8 is U+0023. And latin capital "A" is U+0041, so 23 < 41. In order to make FRC would place "#"-like object to highest index section we need to pass highest UTF-8 symbol. In order to this source http://www.utf8-chartable.de/unicode-utf8-table.pl that UTF-8 symbol is U+1000FF (􀃿). Of course there almost is no way that this symbol will occur in real life. Lets use U+100000 for clearness.

Sort name update method looks something like this:

#define UT8_MAX @"\U00100000"

- (void)updateSortName
{
    NSMutableString *prSortName = [NSMutableString stringWithString:[self dataDependantSortName]]; // for sort descriptors

    NSString *prSectionIdentifier = [[prSortName substringToIndex:1] uppercaseString]; // section keypath

    UILocalizedIndexedCollation *collation = [UILocalizedIndexedCollation currentCollation];

    NSUInteger sectionIndex = [collation sectionForObject:prSectionIdentifier collationStringSelector:@selector(stringValue)]; // stringValue is NSString category method that returns [NSString stringWithString:self]

    if(sectionIndex == [[collation sectionTitles] count] - 1) // last section tile '#'
    {
        prSectionIdentifier = UT8_MAX;
    }
    else
    {
        prSectionIdentifier = [collation sectionTitles][sectionIndex];
    }

    [prSortName replaceCharactersInRange:NSMakeRange(0, 1) withString:prSectionIdentifier];

//    sortName, sectionIdentifier - non-transient string attributes in CoreData model

    [self willChangeValueForKey:@"sortName"];
    [self setPrimitiveValue:prSortName forKey:@"sortName"];
    [self didChangeValueForKey:@"sortName"];

    [self willChangeValueForKey:@"sectionIdentifier"];
    [self setPrimitiveValue:prSectionIdentifier forKey:@"sectionIdentifier"];
    [self didChangeValueForKey:@"sectionIdentifier"];
}

FRC setup:

- (void)setupFRC
{
    NSEntityDescription *entityDescription =
    [NSEntityDescription entityForName:@"entity"
                inManagedObjectContext:self.moc];

    NSSortDescriptor *sortNameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"sortName" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)]; // or any selector you need
    NSArray *sortDescriptors = [NSArray arrayWithObjects:sortNameDescriptor, nil];

    NSFetchRequest *fetchRequest = [NSFetchRequest new];
    [fetchRequest setEntity:entityDescription];
    [fetchRequest setFetchBatchSize:BATCH_SIZE];
    [fetchRequest setSortDescriptors:sortDescriptors];

    NSFetchedResultsController *fetchedResultsController =
    [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                        managedObjectContext:self.moc
                                          sectionNameKeyPath:@"sectionIdentifier"
                                                   cacheName:nil];
    self.fetchedResultsController = fetchedResultsController;
}

FRC delegate methods are default. TV delegate and data source methods:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
    return [[self localizedIndexedCollation] sectionTitles];
}

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
    NSString *indexTitle = [title isEqualToString:@"#"] ? UT8_MAX : title;
    NSInteger fetchTitleIndex = NSNotFound;

    NSArray *sections = [self.fetchedResultsController sections];
    for (id <NSFetchedResultsSectionInfo> sectionInfo in sections)
    {
        if([[sectionInfo name] isEqualToString:indexTitle])
        {
            fetchTitleIndex = [sections indexOfObject:sectionInfo];
            break;
        }
    }

    return fetchTitleIndex;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
    NSString *fetchTitle = [sectionInfo name];

    NSInteger collationTitleIndex = [[self localizedIndexedCollation] sectionForObject:fetchTitle
                                                               collationStringSelector:@selector(stringValue)];
    return [[[self localizedIndexedCollation] sectionTitles] objectAtIndex:collationTitleIndex];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}

Thats it. So far works well. Maybe it will work for you.

Upvotes: 2

Scott Gardner
Scott Gardner

Reputation: 8739

Since you cannot sort on a transient property, the solution I implemented is...

  1. Create a string attribute called "sectionKey" for each sortable attribute within each entity in your Core Data model. The sectionKey attribute will be a calculated value derived from a base attribute (e.g., a name or title attribute). It must be persisted because (currently) a transient property cannot be used in a sort descriptor for a fetch request. Enable indexing on each sectionKey and base attribute for which sorting will be offered. In order to apply this update to an existing app, you will need to perform a lightweight migration, and also include a routine to update pre-existing databases.

  2. If you are seeding data (e.g., to populate new installs with a standard set of data, or to create localized SQLite databases for each target language, of which one will be copied over on initial launch), in that code, calculate and update each entity's sectionKey attribute(s). Opinions vary as to the "best" approach to seeding data, however it's worth noting that a handful of plist files for each language (which will typically range from a few bytes to 20k, even for a list comprised of several hundred values) will leave a much smaller overall footprint than an individual SQLite database for each language (which start at about 20k each). On a side note, Microsoft Excel for Mac can be configured to provide localized sorting of lists by enabling the language features (3).

  3. In the fetched results controller constructor, sort on the sectionKey and base attribute, and pass the sectionKey for the section name key path.

  4. Add the calculation logic to update the sectionKey attribute(s) in all add or edit user inputs, for example, in textFieldDidEndEditing:.

That's it! No manual partitioning of fetched objects into an array of arrays. NSFetchedResultsController will do the localized collation for you. For example, in the case of Chinese (Simplified), the fetched objects will be indexed by phonetic pronunciation (4).

(1) From Apple IOS Developer Library > Internationalization Programming Topics > Internationalization and Localization. (2) 3_SimpleIndexedTableView of the TableViewSuite. (3) How to enable Chinese language features in Microsoft Office for Mac. (4) The Chinese language is commonly sorted by either stroke count or phonetic pronunciation.

Upvotes: 6

Maurizio
Maurizio

Reputation: 31

Brent, my solution is based on FRC and I get sectioning from the fetch specifying a transient attribute on my model object that returns the section name for the object. I use UIlocalizedIndexedCollation only in the implementation of the attribute getter then I rely on the FRC implementation on the table view controller. Of course I use localizedCaseInsensitiveCompare as sorting selector on the fetch.

- (NSString *)sectionInitial {

    NSInteger idx = [[UILocalizedIndexedCollation currentCollation] sectionForObject:self     collationStringSelector:@selector(localeName)];
    NSString *collRet = [[[UILocalizedIndexedCollation currentCollation] sectionTitles]     objectAtIndex:idx];

    return collRet;
}

The only drawback I have is that i can't have the # section at the end because I don't change the sorting from the DB. Everything else works well.

Upvotes: 3

Related Questions