Tutorial :Rearranging UITableView with Core Data [duplicate]



Question:

Possible Duplicate:
How to implement re-ordering of CoreData records?

I'm trying to find a code sample that shows how to handle moving/rearranging cells in a tableView when the cell uses a fetchedResultsController (i.e. in conjunction with Core Data). I'm getting the moveRowAtIndexPath: call to my data source, but I can't find the right combination of black magic to get the table/data to recognize the change properly.

For example, when I move row 0 to row 2 and then let go, it "looks" correct. Then I click "Done". The row (1) that had slid up to fill row 0 still has it's editing mode appearance (minus and move icons), while the other rows below slide back to normal appearance. If I then scroll down, as row 2 (originally 0, remember?) nears the top, it completely disappears.

WTF. Do I need to somehow invalidate the fetchedResultsController? Whenever I set it to nil, I get crashes. Should I release it instead? Am I in the weeds?

Here's what I've currently got in there...

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {        NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];        /*       Update the links data in response to the move.       Update the display order indexes within the range of the move.       */        if (fromIndexPath.section == toIndexPath.section) {            NSInteger start = fromIndexPath.row;          NSInteger end = toIndexPath.row;          NSInteger i = 0;          if (toIndexPath.row < start)              start = toIndexPath.row;          if (fromIndexPath.row > end)              end = fromIndexPath.row;          for (i = start; i <= end; i++) {              NSIndexPath *tempPath = [NSIndexPath indexPathForRow:i inSection:toIndexPath.section];              LinkObj *link = [fetchedResultsController objectAtIndexPath:tempPath];              //[managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:tempPath]];              link.order = [NSNumber numberWithInteger:i];              [managedObjectContext refreshObject:link mergeChanges:YES];              //[managedObjectContext insertObject:link];          }        }      // Save the context.      NSError *error;      if (![context save:&error]) {          // Handle the error...      }    }    - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {        // The fetch controller is about to start sending change notifications, so prepare the table view for updates.      if (self.theTableView != nil)          [self.theTableView beginUpdates];  }    - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {      // The fetch controller has sent all current change notifications, so tell the table view to process all updates.      if (self.theTableView != nil) {          [self.theTableView endUpdates];      }  }  


Solution:1

Usually when you see artifacts like that what is going on is the UI has animated to a new position and told you about it, then the updates you have done to your model don't correctly reflect the state which results in glitches the next time the view has to refer to the model for an update.

I think you don't exactly understand what you are supposed to do in the method. It is called because the UI has changed and it needs to let the model to change accordingly. The code below presumes the results are already in the new order and you just need to reset the order field for some reason:

    for (i = start; i <= end; i++) {              NSIndexPath *tempPath = [NSIndexPath indexPathForRow:i inSection:toIndexPath.section];              LinkObj *link = [fetchedResultsController objectAtIndexPath:tempPath];              //[managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:tempPath]];              link.order = [NSNumber numberWithInteger:i];              [managedObjectContext refreshObject:link mergeChanges:YES];              //[managedObjectContext insertObject:link];      }  

The catch is that you are not actually changing the order in the underlying model. Those indexPaths are from UITableViewController, it is telling you that the user dragged between those to spots and you need to update the underlying data according. But the fetchedResultsController is always in sort order, so until you have changed those properties nothing has moved.

The thing is, they have not been moved, you are being called to tell you that you need to move them around (by adjusting the sortable property). You really need to something more like:

NSNumber *targetOrder = [fetchedResultsController objectAtIndexPath:toIndexPath];  LinkObj *link = [fetchedResultsController objectAtIndexPath:FromPath];  link.order = targetOrder;  

Which will cause the objects to reorder, then go through and clean up any of the order numbers of other objects that should have shifted up, being aware the indexes may have moved.


Solution:2

Here's what's officially working now, with deletes, moves, and inserts. I "validate" the order any time there's an edit action affecting the order.

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {      if (indexPath.section != kHeaderSection) {            if (editingStyle == UITableViewCellEditingStyleDelete) {                @try {                  LinkObj * link = [self.fetchedResultsController objectAtIndexPath:indexPath];                    debug_NSLog(@"Deleting at indexPath %@", [indexPath description]);              //debug_NSLog(@"Deleting object %@", [link description]);                    if ([self numberOfBodyLinks] > 1)                       [self.managedObjectContext deleteObject:link];                }              @catch (NSException * e) {                  debug_NSLog(@"Failure in commitEditingStyle, name=%@ reason=%@", e.name, e.reason);              }            }          else if (editingStyle == UITableViewCellEditingStyleInsert) {              // we need this for when they click the "+" icon; just select the row              [theTableView.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];          }      }  }    - (BOOL)validateLinkOrders {              NSUInteger index = 0;      @try {                NSArray * fetchedObjects = [self.fetchedResultsController fetchedObjects];            if (fetchedObjects == nil)              return NO;            LinkObj * link = nil;                 for (link in fetchedObjects) {              if (link.section.intValue == kBodySection) {                  if (link.order.intValue != index) {                      debug_NSLog(@"Info: Order out of sync, order=%@ expected=%d", link.order, index);                        link.order = [NSNumber numberWithInt:index];                  }                  index++;              }          }      }      @catch (NSException * e) {          debug_NSLog(@"Failure in validateLinkOrders, name=%@ reason=%@", e.name, e.reason);      }      return (index > 0 ? YES : NO);  }      - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {      NSArray * fetchedObjects = [self.fetchedResultsController fetchedObjects];        if (fetchedObjects == nil)          return;        NSUInteger fromRow = fromIndexPath.row + NUM_HEADER_SECTION_ROWS;      NSUInteger toRow = toIndexPath.row + NUM_HEADER_SECTION_ROWS;        NSInteger start = fromRow;      NSInteger end = toRow;      NSInteger i = 0;      LinkObj *link = nil;        if (toRow < start)          start = toRow;      if (fromRow > end)          end = fromRow;        @try {            for (i = start; i <= end; i++) {              link = [fetchedObjects objectAtIndex:i]; //              //debug_NSLog(@"Before: %@", link);                if (i == fromRow)   // it's our initial cell, just set it to our final destination                  link.order = [NSNumber numberWithInt:(toRow-NUM_HEADER_SECTION_ROWS)];              else if (fromRow < toRow)                  link.order = [NSNumber numberWithInt:(i-1-NUM_HEADER_SECTION_ROWS)];        // it moved forward, shift back              else // if (fromIndexPath.row > toIndexPath.row)                  link.order = [NSNumber numberWithInt:(i+1-NUM_HEADER_SECTION_ROWS)];        // it moved backward, shift forward              //debug_NSLog(@"After: %@", link);          }      }      @catch (NSException * e) {          debug_NSLog(@"Failure in moveRowAtIndexPath, name=%@ reason=%@", e.name, e.reason);      }  }      - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {          @try {          switch (type) {              case NSFetchedResultsChangeInsert:                  [theTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];                  [self validateLinkOrders];                  break;              case NSFetchedResultsChangeUpdate:                  break;              case NSFetchedResultsChangeMove:                  self.moving = YES;                  [self validateLinkOrders];                  break;              case NSFetchedResultsChangeDelete:                  [theTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];                  [self validateLinkOrders];                  break;              default:                  break;          }      }      @catch (NSException * e) {          debug_NSLog(@"Failure in didChangeObject, name=%@ reason=%@", e.name, e.reason);      }  }    - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {      switch(type) {          case NSFetchedResultsChangeInsert:              [self.theTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];              break;            case NSFetchedResultsChangeDelete:              [self.theTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];              break;      }  }    - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {      // The fetch controller has sent all current change notifications, so tell the table view to process all updates.      @try {          if (self.theTableView != nil) {              //[self.theTableView endUpdates];              if (self.moving) {                  self.moving = NO;                  [self.theTableView reloadData];                  //[self performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];              }              [self performSelector:@selector(save) withObject:nil afterDelay:0.02];          }           }      @catch (NSException * e) {          debug_NSLog(@"Failure in controllerDidChangeContent, name=%@ reason=%@", e.name, e.reason);      }  }  


Solution:3

When you move a row in the table view, you actually move a block of other rows (consisting of at least one row) into the other direction at the same time. The trick is to only update the displayOrder property of this block and of the moved item.

First, ensure that the displayOrder property of all rows is set according to the tables current display order. We don't have to save the context here, we will save it later when the actual move operation finished:

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {      [super setEditing:editing animated:animated];      [_tableView setEditing:editing animated:animated];      if(editing) {          NSInteger rowsInSection = [self tableView:_tableView numberOfRowsInSection:0];         // Update the position of all items         for (NSInteger i=0; i<rowsInSection; i++) {            NSIndexPath *curIndexPath = [NSIndexPath indexPathForRow:i inSection:0];            SomeManagedObject *curObj = [_fetchedResultsController objectAtIndexPath:curIndexPath];            NSNumber *newPosition = [NSNumber numberWithInteger:i];            if (![curObj.displayOrder isEqualToNumber:newPosition]) {               curObj.displayOrder = newPosition;            }         }      }  }  

Then the only thing you have to do is to update the position of the moved item and of all items between fromIndexPath and toIndexPath:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {      NSInteger moveDirection = 1;      NSIndexPath *lowerIndexPath = toIndexPath;      NSIndexPath *higherIndexPath = fromIndexPath;      if (fromIndexPath.row < toIndexPath.row) {          // Move items one position upwards          moveDirection = -1;          lowerIndexPath = fromIndexPath;          higherIndexPath = toIndexPath;      }        // Move all items between fromIndexPath and toIndexPath upwards or downwards by one position      for (NSInteger i=lowerIndexPath.row; i<=higherIndexPath.row; i++) {          NSIndexPath *curIndexPath = [NSIndexPath indexPathForRow:i inSection:fromIndexPath.section];          SomeManagedObject *curObj = [_fetchedResultsController objectAtIndexPath:curIndexPath];          NSNumber *newPosition = [NSNumber numberWithInteger:i+moveDirection];          curObj.displayOrder = newPosition;      }        SomeManagedObject *movedObj = [_fetchedResultsController objectAtIndexPath:fromIndexPath];      movedObj.displayOrder = [NSNumber numberWithInteger:toIndexPath.row];      NSError *error;      if (![_fetchedResultsController.managedObjectContext save:&error]) {          NSLog(@"Could not save context: %@", error);      }  }  


Solution:4

The best answer is actually in Clint Harris's comment on the question:

http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller

To quickly summarise, the essential part is to have a displayOrder property on the objects you are trying to rearrange with the sort description for the fetched results controller ordering on that field. The code for moveRowAtIndexPath:toIndexPath: then looks like this:

- (void)tableView:(UITableView *)tableView   moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath         toIndexPath:(NSIndexPath *)destinationIndexPath;  {      NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];      // Grab the item we're moving.    NSManagedObject *thing = [[self fetchedResultsController] objectAtIndexPath:sourceIndexPath];      // Remove the object we're moving from the array.    [things removeObject:thing];    // Now re-insert it at the destination.    [things insertObject:thing 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 things)    {      [mo setValue:[NSNumber numberWithInt:i++] forKey:@"displayOrder"];    }      [things release], things = nil;      [managedObjectContext save:nil];  }  

The Apple documentation also contains important hints:

https://developer.apple.com/library/ios/#documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html

This is also mentioned in How to implement re-ordering of CoreData records?

To quote the Apple documentation:

User-Driven Updates

In general, NSFetchedResultsController is designed to respond to changes at the model layer. If you allow a user to reorder table rows, then your implementation of the delegate methods must take this into account.

Typically, if you allow the user to reorder table rows, your model object has an attribute that specifies its index. When the user moves a row, you update this attribute accordingly. This, however, has the side effect of causing the controller to notice the change, and so inform its delegate of the update (using controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:). If you simply use the implementation of this method shown in “Typical Use,” then the delegate attempts to update the table view. The table view, however, is already in the appropriate state because of the user’s action.

In general, therefore, if you support user-driven updates, you should set a flag if a move is initiated by the user. In the implementation of your delegate methods, if the flag is set, you bypass main method implementations; for example:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject      atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type      newIndexPath:(NSIndexPath *)newIndexPath {        if (!changeIsUserDriven) {          UITableView *tableView = self.tableView;          // Implementation continues...  


Solution:5

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{      [self.pairs exchangeObjectAtIndex:sourceIndexPath.row withObjectAtIndex:destinationIndexPath.row];      [self performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];  }    - (void)reloadData{      [table reloadData];  }  

The table can not reload while it is moving, reload after a delay and you will be fine.


Solution:6

Sorry Greg, I am sure I am doing something wrong, but you answer doesn't work for me.

Although all my objects validate properly, when I exit edit mode, one of the rows freezes (the editing controls don't disappear) and the cells don't respond properly afterwards.

Maybe my problem is that I don't know how to use the moving property that you set (self.moving = YES). Could you please clarify this? Thank you very much.

Jorge


Solution:7

This is very difficult way to implement such functionality. Much easier and elegant way can be found here: UITableView Core Data reordering


Solution:8

[<> isEditing] can be used to determine whether editing of the table is enabled or not. Rather than delaying it as suggested by using the following statement

[table performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];


Note:If u also have question or solution just comment us below or mail us on toontricks1994@gmail.com
Previous
Next Post »