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

后端 未结 4 1505
说谎
说谎 2020-12-12 18:01

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

相关标签:
4条回答
  • 2020-12-12 18:15

    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;
    
        ...
    
      }
    }
    
    0 讨论(0)
  • 2020-12-12 18:18

    I think that:

        affectedObject.displayOrderValue = toIndex;
    

    must be placed after:

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

    and before:

        [self FF_fetchResults];  
    
    0 讨论(0)
  • 2020-12-12 18:20

    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];  
    }
    
    0 讨论(0)
  • 2020-12-12 18:28

    (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.

    0 讨论(0)
提交回复
热议问题