views:

6744

answers:

5

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];
    }
}
+4  A: 

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.

Louis Gerbarg
+2  A: 

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);
    }
}
Greg Combs
Thanks Greg, the above code is working for me. I have to change the controllerDidChangeContent code to add in endUpdates to make delete and insert work for the ui update:if (self.moving) { self.moving = NO; [self.theTableView reloadData]; [self performSelector:@selector(save) withObject:nil afterDelay:0.02];}else { [self.theTableView endUpdates];}
Gaius Parx
A: 
- (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.

This still brought exceptions, see my updated "answer" for what's working now.
Greg Combs
Sorry wish I knew what was happening. I always us mutable arrays for this which seems less complicated but doesn't really serve your code sample well. *sigh*
Why don't you do this instead `[table performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];` so you don't have to make another method :)
Sam Soffes
this is worked but seem not good effect , whole view will be refresh.looks the choice answer in this thread is great but I think seem 'insert' and 'delete' is not necessary, I just use a field 'displayIndex' to control the order, if a row move to another position in the list, I just use bubble sort way change the field value , that is works fine no 'insert' and 'delete' is required
Robin
A: 

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

Jorge Ortiz
I've seen that exact same thing before. Unfortunately, I haven't dug into my sources in weeks and I don't keep them here at work anymore. I'll see if I can track down how I use self.moving but if I remember, it was to delay updating the core data store / fetched results until after editing/moving was complete. After that point, I make sure the "order" property is consistent with what it should be then update the core data store / fetched results, then reload/refresh the data displayed in the table AGAIN so that the visual inconsistencies are cleared up.This is just from memory, I'll check.
Greg Combs
Looking at everything now... Initialize moving to NO. Set moving to YES and validateLinkOrder when we detect didObJectChange/NSFetchedResultsChangeMove. Don't do anything in controllerWillChangeContent. But in controllerDidChangeContent, see see if moving is YES, if so, we set it to NO, tell the tableView to reloadData, and perform the "save" selector after a delay of 0.02. The "save" selector basically tells the managedObjectContext to save and log any errors, but it does this within a try/catch exception handling if things go bad.
Greg Combs
See my revised code listing in the "checked" answer above.
Greg Combs
Finally got it to work. My problem was that was storing an int in the order attribute instead of an NSNumber. Those Core Data crashes are quite ugly and the information is pretty unhelpful.Thanks a lot!!!
Jorge Ortiz
A: 

This is very difficult way to implement such functionality. Much easier and elegant way can be found here: http://stackoverflow.com/questions/2360938/uitableview-core-data-reordering/2442356#2442356

AlexS
Well, naturally things get a *lot* simpler if you don't use a fetchedResultsController with reordering. At least, simpler when it comes to the reordering part of it. I figure if using a fetchedResultsController is recommended by the AppleDev administration, then we ought to have a graceful way of rearranging. Unfortunately, it seems that although these two things are compatible, as shown above, they are certainly not graceful, as shown above.
Greg Combs