How to implement re-ordering of CoreData records?

2019-01-03 20:44发布

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 core-data
10条回答
戒情不戒烟
2楼-- · 2019-01-03 20:54

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];
}
查看更多
一纸荒年 Trace。
3楼-- · 2019-01-03 20:58

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];
}
查看更多
够拽才男人
4楼-- · 2019-01-03 21:02

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 :)

查看更多
祖国的老花朵
5楼-- · 2019-01-03 21:02

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];

}

查看更多
6楼-- · 2019-01-03 21:08

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.

查看更多
在下西门庆
7楼-- · 2019-01-03 21:12

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;
查看更多
登录 后发表回答