How to implement re-ordering of CoreData records?

Viewed 23034

I am using CoreData for my iPhone app, but CoreData doesn't provide an automatic way of allowing you to reorder the records. I thought of using another column to store the order info, but using contiguous numbers for ordering index has a problem. if I am dealing with lots of data, reordering a record potentially involves updating a lot of records on the ordering info (it's sorta like changing the order of an array element)

What's the best way to implement an efficient ordering scheme?

10 Answers

FetchedResultsController and its delegate are not meant to be used for user-driven model changes. See the Apple reference doc. Look for User-Driven Updates part. So if you look for some magical, one-line way, there's not such, sadly.

What you need to do is make updates in this method:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
 userDrivenDataModelChange = YES;

 ...[UPDATE THE MODEL then SAVE CONTEXT]...

 userDrivenDataModelChange = NO;
}

and also prevent the notifications to do anything, as changes are already done by the user:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
 if (userDrivenDataModelChange) return;
 ...
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
 if (userDrivenDataModelChange) return;
 ...
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
 if (userDrivenDataModelChange) return;
 ...
}

I have just implemented this in my to-do app (Quickie) and it works fine.

A late reply: perhaps you could store the sort key as a string. Inserting a record between two existing rows can be done trivially by adding an additional character to a string, e.g. inserting "AM" between the rows "A" and "B". No reordering is required. A similar idea could be accomplished by using a floating point number or some simple bit arithmetic on a 4-byte integer: insert a row with a sort key value that is half way between the adjacent rows.

Pathological cases could arise where the string is too long, the float is too small, or there is no more room in the int, but then you could just renumber the entity and make a fresh start. A scan through and update of all your records on a rare occasion is much better than faulting every object every time a user reorders.

For example, consider int32. Using the high 3 bytes as the initial ordering gives you almost 17 million rows with the ability to insert up to 256 rows between any two rows. 2 bytes allows inserting 65000 rows between any two rows before a rescan.

Here's the pseudo-code I have in mind for a 2 byte increment and 2 bytes for inserting:

AppendRow:item
    item.sortKey = tail.sortKey + 0x10000

InsertRow:item betweenRow:a andNextRow:b
    item.sortKey = a.sortKey + (b.sortKey - a.sortKey) >> 1

Normally you would be calling AppendRow resulting in rows with sortKeys of 0x10000, 0x20000, 0x30000, etc. Sometimes you would have to InsertRow, say between the first and the second, resulting in a sortKey of 0x180000.

I finally gave up on FetchController in edit mode since I need to reorder my table cells as well. I'd like to see an example of it working. Instead I kept with having a mutablearray being the current view of the table, and also keeping the CoreData orderItem atrribute consistent.

NSUInteger fromRow = [fromIndexPath row]; 
NSUInteger toRow = [toIndexPath row]; 



 if (fromRow != toRow) {

    // array up to date
    id object = [[eventsArray objectAtIndex:fromRow] retain]; 
    [eventsArray removeObjectAtIndex:fromRow]; 
    [eventsArray insertObject:object atIndex:toRow]; 
    [object release]; 

    NSFetchRequest *fetchRequestFrom = [[NSFetchRequest alloc] init];
    NSEntityDescription *entityFrom = [NSEntityDescription entityForName:@"Lister" inManagedObjectContext:managedObjectContext];

    [fetchRequestFrom setEntity:entityFrom];

    NSPredicate *predicate; 
    if (fromRow < toRow) predicate = [NSPredicate predicateWithFormat:@"itemOrder >= %d AND itemOrder <= %d", fromRow, toRow];  
    else predicate = [NSPredicate predicateWithFormat:@"itemOrder <= %d AND itemOrder >= %d", fromRow, toRow];                          
    [fetchRequestFrom setPredicate:predicate];

    NSError *error;
    NSArray *fetchedObjectsFrom = [managedObjectContext executeFetchRequest:fetchRequestFrom error:&error];
    [fetchRequestFrom release]; 

    if (fetchedObjectsFrom != nil) { 
        for ( Lister* lister in fetchedObjectsFrom ) {

            if ([[lister itemOrder] integerValue] == fromRow) { // the item that moved
                NSNumber *orderNumber = [[NSNumber alloc] initWithInteger:toRow];               
                [lister setItemOrder:orderNumber];
                [orderNumber release];
            } else { 
                NSInteger orderNewInt;
                if (fromRow < toRow) { 
                    orderNewInt = [[lister itemOrder] integerValue] -1; 
                } else { 
                    orderNewInt = [[lister itemOrder] integerValue] +1; 
                }
                NSNumber *orderNumber = [[NSNumber alloc] initWithInteger:orderNewInt];
                [lister setItemOrder:orderNumber];
                [orderNumber release];
            }

        }

        NSError *error;
        if (![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();  // Fail
        }           

    }                                   

}   

If anyone has a solution using fetchController please post it.

Try having a look at the Core Data tutorial for iPhone here. One of the sections there talk about sorting (using NSSortDescriptor).

You may also find the Core Data basics page to be useful.

Related