views:

1899

answers:

5

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?

A: 

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.

Timothy Walters
It's not sorting though, it's re-ordering. Sorting is easy, but re-ordering involves potentially lots of updates unless I use a double linked list approach, which I am not sure how to translate to CoreData.
Boon
Is there a reason you don't want to re-query with a new Sort instruction? It's accessing local data anyway, so it's not like it's a big performance hit.
Timothy Walters
It's really not about avoid Sort. The main question is, how do I implement ordering in the first place? If I add order index into the schema, I run into the situation of having to do a lot of recalculation when I re-order a row. Just imagine this with re-ordering an array and you will see what I mean -- when you move an array element to another position, everything after it has to be moved as well.
Boon
A: 

I have this question as well. I have a tableview populated with a NSFetchedResultsController. Its easy enough to allow the user to reorder the table elemenents, but how to save this back to the coredata store? So next time the app starts it appears in the new order.

I was hoping for some magic, but I guess I'll revert to the old method of just having an Order Property in the Model, then manually maintain the Order as items are moved up or down. Would be great if this was built into Core Data since it does so much already.

A: 

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]);
   exit(-1);  // Fail
  }   

 }         

}

If anyone has a solution using fetchController please post it.

+1  A: 

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.

dk
+10  A: 

FetchedResultsController and its delegate are not meant to be used for user-driven model changes. See this. 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 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.

Aleksandar Vacic
+1 This was the key for me to implement the "changeIsUserDriven" mentioned in the docs. Thanks.
hanleyp
Also, see http://stackoverflow.com/questions/1648223/how-can-i-maintain-display-order-in-uitableview-using-core-data/1648504#1648504
gerry3