Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I maintain display order in UITableView using Core Data?

I'm having some trouble getting my Core Data entities to play nice and order when using an UITableView.

I've been through a number of tutorials and other questions here on StackOverflow, but there doesn't seem to be a clear or elegant way to do this - I'm really hoping I'm missing something.

I have a single Core Data entity that has an int16 attribute on it called "displayOrder". I use an NSFetchRequest that has been sorted on "displayOrder" to return the data for my UITableView. Everything but reordering is being respected. Here is my (inefficient) moveRowAtIndePath method:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {         

    NSUInteger fromIndex = fromIndexPath.row;  
    NSUInteger toIndex = toIndexPath.row;

    FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];  
    affectedObject.displayOrderValue = toIndex;

    [self FF_fetchResults];


    for (NSUInteger i = 0; i < [self.fetchedResultsController.fetchedObjects count]; i++) {  
        FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];  
        NSLog(@"Updated %@ / %@ from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, i);  
        otherObject.displayOrderValue = i;  
    }

    [self FF_fetchResults];  
}

Can anyone point me in the direction of a good bit of sample code, or see what I'm doing wrong? The tableview display updates OK, and I can see through my log messages that the displayOrder property is being updated. It's just not consistently saving and reloading, and something feels very "off" about this implementation (aside from the wasteful iteration of all of my FFObjects).

Thanks in advance for any advice you can lend.

like image 233
Tony Arnold Avatar asked Oct 30 '09 05:10

Tony Arnold


3 Answers

I took a look at your code and this might work better:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {         

    NSUInteger fromIndex = fromIndexPath.row;  
    NSUInteger toIndex = toIndexPath.row;

    if (fromIndex == toIndex) {
        return;
    }

    FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];  
    affectedObject.displayOrderValue = toIndex;

    NSUInteger start, end;
    int delta;

    if (fromIndex < toIndex) {
        // move was down, need to shift up
        delta = -1;
        start = fromIndex + 1;
        end = toIndex;
    } else { // fromIndex > toIndex
        // move was up, need to shift down
        delta = 1;
        start = toIndex;
        end = fromIndex - 1;
    }

    for (NSUInteger i = start; i <= end; i++) {
        FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];  
        NSLog(@"Updated %@ / %@ from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);  
        otherObject.displayOrderValue += delta;
    }

    [self FF_fetchResults];  
}
like image 97
gerry3 Avatar answered Nov 15 '22 14:11

gerry3


(This is intended as as comment on gerry3's answer above, but I am not yet able to comment on other users' questions and answers.)

A small improvement for gerry3's - very elegant - solution. If I'm not mistaken, the line

otherObject.displayOrderValue += delta;

will actually perform pointer arithmetic if displayOrderValue is not of primitive type. Which may not be what you want. Instead, to set the value of the entity, I propose:

otherObject.displayOrderValue = [NSNumber numberWithInt:[otherObject.displayOrderValue intValue] + delta];

This should update your entity property correctly and avoid any EXC_BAD_ACCESS crashes.

like image 41
octy Avatar answered Nov 15 '22 13:11

octy


Here a full solution how to manage an indexed table with core data. Your attribute is called displayOrder, I call it index. First of all, you better separate view controller and model. For this I use a model controller, which is the interface between the view and the model.

There are 3 cases you need to manage that the user can influence via the view controller.

  1. Adding a new object
  2. Deleting an existing object
  3. Reorder objects.

The first two cases Adding and Deleting are pretty straightforward. Delete calls a routine called renewObjectIndicesUpwardsFromIndex in order to update the indices after the deleted object.

- (void)createObjectWithTitle:(NSString*)title {
    FFObject* object = [FFObject insertIntoContext:self.managedObjectContext];

    object.title = title;
    object.index = [NSNumber numberWithInteger:[self numberTotalObjects]];
    [self saveContext];
}

- (void)deleteObject:(FFObject*)anObject {
  NSInteger objectIndex = [anObject.index integerValue];
  [anObject deleteObject];
  [self renewObjectIndicesUpwardsFromIndex:objectIndex];
  [self saveContext];
}

- (void)renewObjectIndicesUpwardsFromIndex:(NSInteger)fromIndex {
  NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
  [fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];

  NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index > %d)", fromIndex];
  [fetchRequest setPredicate:predicate];

  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
  NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];

  [fetchRequest setSortDescriptors:sortDescriptors];

  NSError* fetchError = nil;
  NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

  NSInteger index = fromIndex;
  for (FFObject* object in objects) {
    object.index = [NSNumber numberWithInteger:index];
    index += 1;
  }
  [self saveContext];
}

Before I come to the controller routines for the re-order, here the part in the view controller. I use a bool isModifyingOrder similar to this answer. Notice that the view controller calls two functions in the controller moveObjectOrderUp and moveObjectOrderDown. Depending on how you display the objects in the table view - newest first or newest last - you can switch them.

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

  isModifyingOrder = YES;

  NSUInteger fromIndex = sourceIndexPath.row;
  NSUInteger toIndex = destinationIndexPath.row;

  if (fromIndex == toIndex) {
    return;
  }

  FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];

  NSInteger delta;
  if (fromIndex < toIndex) {
    delta = toIndex - fromIndex;
    NSLog(@"Moved down by %lu cells", delta);
    [self.objectController moveObjectOrderUp:affectedObject by:delta];
  } else {
    delta = fromIndex - toIndex;
    NSLog(@"Moved up by %lu cells", delta);
    [self.objectController moveObjectOrderDown:affectedObject by:delta];
  }

  isModifyingOrder = NO;
}

And here the part in the controller. This can be written nicer, but for understanding this is maybe best.

- (void)moveObjectOrderUp:(FFObject*)affectedObject by:(NSInteger)delta {
  NSInteger fromIndex = [affectedObject.index integerValue] - delta;
  NSInteger toIndex = [affectedObject.index integerValue];

  if (fromIndex < 1) {
    return;
  }

  NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
  [fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];

  NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index >= %d) AND (index < %d)", fromIndex, toIndex];
  [fetchRequest setPredicate:predicate];

  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
  NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];

  [fetchRequest setSortDescriptors:sortDescriptors];

  NSError* fetchError = nil;
  NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

  for (FFObject* object in objects) {
    NSInteger newIndex = [object.index integerValue] + 1;
    object.index = [NSNumber numberWithInteger:newIndex];
  }

  affectedObject.index = [NSNumber numberWithInteger:fromIndex];

  [self saveContext];
}

- (void)moveObjectOrderDown:(FFObject*)affectedObject by:(NSInteger)delta {
  NSInteger fromIndex = [affectedObject.index integerValue];
  NSInteger toIndex = [affectedObject.index integerValue] + delta;

  NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
  [fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];

  NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index > %d) AND (index <= %d)", fromIndex, toIndex];
  [fetchRequest setPredicate:predicate];

  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
  NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];

  [fetchRequest setSortDescriptors:sortDescriptors];

  NSError* fetchError = nil;
  NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

  for (FFObject* object in objects)
  {
    NSInteger newIndex = [object.index integerValue] - 1;
    object.index = [NSNumber numberWithInteger:newIndex];
  }

  affectedObject.index = [NSNumber numberWithInteger:toIndex];

  [self saveContext];
}

Don't forget to use a second BOOL in your view controller for the delete action to prevent the move notification to do anything. I call it isDeleting and put it here.

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath {
  if (isModifyingOrder) return;

  ...

  switch(type) {

    ...

    case NSFetchedResultsChangeMove:
        if (isDeleting == false) {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:localIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:localNewIndexPath]withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

    ...

  }
}
like image 38
Herbert Bay Avatar answered Nov 15 '22 14:11

Herbert Bay