How to implement re-ordering of CoreData records?

IosCore Data

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

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.

Solution 2 - Ios

Here is a quick example showing a way to dump the fetched results into an NSMutableArray which you use to move the cells around. Then you just update an attribute on the entity called orderInTable and then save the managed object context.

This way, you don't have to worry about manually changing indexes and instead you let the NSMutableArray handle that for you.

Create a BOOL that you can use to temporarily bypass the NSFetchedResultsControllerDelegate

@interface PlaylistViewController ()
{
    BOOL changingPlaylistOrder;
}
@end

Table view delegate method:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    // Refer to https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40008228-CH1-SW14
    
    // Bypass the delegates temporarily
    changingPlaylistOrder = YES;
    
    // Get a handle to the playlist we're moving
    NSMutableArray *sortedPlaylists = [NSMutableArray arrayWithArray:[self.fetchedResultsController fetchedObjects]];
    
    // Get a handle to the call we're moving
	Playlist *playlistWeAreMoving = [sortedPlaylists objectAtIndex:sourceIndexPath.row];
    
    // Remove the call from it's current position
	[sortedPlaylists removeObjectAtIndex:sourceIndexPath.row];
    
    // Insert it at it's new position
	[sortedPlaylists insertObject:playlistWeAreMoving atIndex:destinationIndexPath.row];
    
    // Update the order of them all according to their index in the mutable array
    [sortedPlaylists enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        Playlist *zePlaylist = (Playlist *)obj;
        zePlaylist.orderInTable = [NSNumber numberWithInt:idx];
    }];
    
    // Save the managed object context
    [commonContext save];
    
    // Allow the delegates to work now
    changingPlaylistOrder = NO;
}

Your delegates would look something like this now:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    if (changingPlaylistOrder) return;
    
    switch(type)
    {
        case NSFetchedResultsChangeMove:
            [self configureCell:(PlaylistCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;
            
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if (changingPlaylistOrder) return;
    
    [self.tableView reloadData];
}

Solution 3 - Ios

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.

Solution 4 - Ios

I adapted this from method from Matt Gallagher's blog (can't find original link). This may not be the best solution if you have millions of records, but will defer saving until the user has finished reordering the records.

- (void)moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath sortProperty:(NSString*)sortProperty
{
    NSMutableArray *allFRCObjects = [[self.frc fetchedObjects] mutableCopy];
    // Grab the item we're moving.
    NSManagedObject *sourceObject = [self.frc objectAtIndexPath:sourceIndexPath];
    
    // Remove the object we're moving from the array.
    [allFRCObjects removeObject:sourceObject];
    // Now re-insert it at the destination.
    [allFRCObjects insertObject:sourceObject atIndex:[destinationIndexPath row]];

    // All of the objects are now in their correct order. Update each
    // object's displayOrder field by iterating through the array.
    int i = 0;
    for (NSManagedObject *mo in allFRCObjects)
    {
        [mo setValue:[NSNumber numberWithInt:i++] forKey:sortProperty];
    }
    //DO NOT SAVE THE MANAGED OBJECT CONTEXT YET

   
}

- (void)setEditing:(BOOL)editing
{
    [super setEditing:editing];
    if(!editing)
        [self.managedObjectContext save:nil];
}

Solution 5 - Ios

I have implemented the approach of @andrew / @dk with the the double values.

You can find the UIOrderedTableView on github.

feel free to fork it :)

Solution 6 - Ios

Actually, there's a much simpler way, use a "double" type as an ordering column.

Then whenever you re-order you only EVER need to reset the value of the order attribute for the reordered item:

reorderedItem.orderValue = previousElement.OrderValue + (next.orderValue - previousElement.OrderValue) / 2.0;

Solution 7 - Ios

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.

Solution 8 - Ios

So having spent some time on this problem...!

The answers above are great building blocks and without them I would have been lost, but as with other respondents I found that they only partially worked. If you implement them you will find that they work once or twice, then error, or you lose data as you go. The answer below is far from perfect - it's the result of quite a lot of late nights, trial and error.

There are some issues with these approaches:

  1. The NSFetchedResultsController linked to NSMutableArray doesn't guarantee that the context will be updated, so you may see that this works sometimes, but not others.

  2. The copy then delete approach for swapping objects is also difficult behaviour to predict. I found references elsewhere to unpredictable behaviour in referencing an object that had been deleted in the context.

  3. If you use the object index row and have sections, then this won't behave properly. Some of the code above uses just the .row property and unfortunately this could refer to more than one row in a yt

  4. Using NSFetchedResults Delegate = nil, is ok for simple applications, but consider that you want to use the delegate to capture changes that will be replicated to a database then you can see that this won't work properly.

  5. Core Data doesn't really support sorting and ordering in the way that a proper SQL database does. The for loop solution above is good, but there should really be a proper way of ordering data - IOS8? - so you need to go into this expecting that your data will be all over the place.

The issues that people have posted in response to these posts relate to a lot of these issues.

I have got a simple table app with sections to 'partially' work - there are still unexplained UI behaviours that I'm working on, but I believe that I have got to the bottom of it...

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath

This is the usual delegate

{
userDrivenDataModelChange = YES;

uses the semaphore mechanism as described above with the if()return structures.

NSInteger sourceRow = sourceIndexPath.row;
NSInteger sourceSection = sourceIndexPath.section;
NSInteger destinationRow = destinationIndexPath.row;
NSInteger destinationSection = destinationIndexPath.section;

Not all of these are used in the code, but it's useful to have them for debugging

NSError *error = nil;
NSIndexPath *destinationDummy;
int i = 0;

Final initialisation of variables

destinationDummy = [NSIndexPath indexPathForRow:0 inSection:destinationSection] ;
// there should always be a row zero in every section - although it's not shown

I use a row 0 in each section that is hidden, this stores the section name. This allows the section to be visible, even when there are no 'live records in it. I use row 0 to get the section name. The code here is a bit untidy, but it does the job.

NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];    
NSManagedObject *currentObject = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
NSManagedObject *targetObject = [self.fetchedResultsController objectAtIndexPath:destinationDummy];

Get the context and source and destination objects

This code then creates a new object which is takes the data from the source, and the section from the destination.

// set up a new object to be a copy of the old one
NSManagedObject *newObject = [NSEntityDescription
                              insertNewObjectForEntityForName:@"List"
                            inManagedObjectContext:context];
NSString *destinationSectionText = [[targetObject valueForKey:@"section"] description];
[newObject setValue:destinationSectionText forKeyPath:@"section"];
[newObject setValue: [NSNumber numberWithInt:9999999] forKey:@"rowIndex"];
NSString *currentItem = [[currentObject valueForKey:@"item"] description];
[newObject setValue:currentItem forKeyPath:@"item"];
NSNumber *currentQuantity =[currentObject valueForKey:@"quantity"] ;
[newObject setValue: currentQuantity forKey:@"rowIndex"];

Now create a new object and save the context - this is cheating the move operation - you might not get the new record in exactly the place it was dropped - but at least it will be in the right section.

// create a copy of the object for the new location
[context insertObject:newObject];
[context deleteObject:currentObject];
if (![context save:&error]) {
    // Replace this implementation with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

Now do the for loop update as described above. Note that the context is saved before I do this - no idea why this is needed, but it didn't work properly when it wasn't!

i = 0;
for (NSManagedObject *mo in [self.fetchedResultsController fetchedObjects] )
{
    [mo setValue:[NSNumber numberWithInt:i++] forKey:@"rowIndex"];
}
if (![context save:&error]) {
    // Replace this implementation with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

Set the semaphore back and update the table

userDrivenDataModelChange = NO;

[tableView reloadData];

}

Solution 9 - Ios

Here's what I'm doing that seems to work. For every entity I have a createDate that is used to sort the table by when it was created. It also acts as a unique key. So on the move all I do is swap the the source and destination dates.

I would expect the table to be properly ordered after doing the saveContext, but what happens is the two cells just lay on top of each other. So I reload the data and the order is corrected. Starting the app from scratch shows the records still in the proper order.

Not sure it's a general solution or even correct, but so far it seems to work.

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
    HomeEntity* source_home = [self getHomeEntityAtIndexPath:sourceIndexPath];
    HomeEntity* destination_home = [self getHomeEntityAtIndexPath:destinationIndexPath];
    NSTimeInterval temp = destination_home.createDate;
    destination_home.createDate = source_home.createDate;
    source_home.createDate = temp;
    
    CoreDataStack * stack = [CoreDataStack defaultStack];
    [stack saveContext];
    [self.tableView reloadData];
}

Solution 10 - Ios

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBoonView Question on Stackoverflow
Solution 1 - IosAleksandar VacićView Answer on Stackoverflow
Solution 2 - IosiwasrobbedView Answer on Stackoverflow
Solution 3 - Iosdk.View Answer on Stackoverflow
Solution 4 - IosArie LitovskyView Answer on Stackoverflow
Solution 5 - IosStephanView Answer on Stackoverflow
Solution 6 - IosAndrewView Answer on Stackoverflow
Solution 7 - IosJotView Answer on Stackoverflow
Solution 8 - Iosuser3519965View Answer on Stackoverflow
Solution 9 - IosTodd HoffView Answer on Stackoverflow
Solution 10 - IosTimothy WaltersView Answer on Stackoverflow