使用核心数据重新排列UITableView

时间:2009-07-20 22:43:30

标签: iphone uitableview core-data

  

可能重复:
  How to implement re-ordering of CoreData records?

我正在尝试找到一个代码示例,该示例演示了当单元格使用fetchedResultsController(即与Core Data结合使用)时如何处理tableView中的移动/重新排列单元格。我正在获取moveRowAtIndexPath:调用我的数据源,但我找不到正确的黑魔法组合,以使表/数据能够正确识别更改。

例如,当我将第0行移到第2行然后放开时,它“看起来”正确。然后我点击“完成”。向上滑动以填充第0行的行(1)仍然具有编辑模式外观(减去和移动图标),而下面的其他行滑回到正常外观。如果我然后向下滚动,因为第2行(原来是0,记得?)接近顶部,它会完全消失。

WTF。我是否需要以某种方式使fetchedResultsController无效?每当我将它设置为零时,我都会崩溃。我应该发布它吗?我在杂草里吗?

这是我目前在那里得到的......

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

8 个答案:

答案 0 :(得分:8)

通常,当您看到类似的工件时,UI已经动画到新位置并告诉您相关信息,那么您对模型所做的更新无法正确反映导致故障的状态下次视图必须引用模型进行更新。

我认为你并不完全明白你应该在这个方法中做些什么。它被调用是因为UI已经改变,它需要让模型相应地改变。下面的代码假设结果已经在新订单中,您只需要重置订单字段,原因如下:

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

问题在于您实际上并未更改基础模型中的顺序。这些indexPath来自UITableViewController,它告诉你用户在那些之间拖动到点,你需要根据更新底层数据。但是fetchedResultsController始终按排序顺序排列,因此在您更改这些属性之前,没有任何内容移动过。

问题是,它们没有被移动,你被叫来告诉你需要移动它们(通过调整可排序属性)。你真的需要更像的东西:

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

这将导致对象重新排序,然后通过并清理应该向上移动的其他对象的任何订单号,意识到索引可能已经移动。

答案 1 :(得分:7)

以下是正式工作的内容,包括删除,移动和插入。只要有影响订单的编辑操作,我就会“验证”订单。

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

答案 2 :(得分:5)

当您在表格视图中移动一行时,实际上会同时将一行其他行(至少包含一行)移动到另一个方向。诀窍是只更新此块和移动项的displayOrder属性。

首先,确保根据表当前显示顺序设置所有行的displayOrder属性。我们不必在这里保存上下文,我们将在实际移动操作完成后保存它:

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

然后,您唯一需要做的就是更新移动项目的位置以及fromIndexPath和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);
    }
}

答案 3 :(得分:5)

最好的答案实际上是克林特哈里斯对这个问题的评论:

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

要快速总结,关键部分是在您尝试重新排列的对象上具有displayOrder属性,并使用该字段上获取的结果控制器排序的排序描述。然后moveRowAtIndexPath:toIndexPath:的代码如下所示:

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

Apple文档还包含重要提示:

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

How to implement re-ordering of CoreData records?

中也提到了这一点

引用Apple文档:

  

用户驱动的更新

     

一般来说,NSFetchedResultsController旨在响应   模型层的变化。如果允许用户重新排序表行,   那么你的委托方法的实现必须把它带入   帐户。

     

通常,如果您允许用户重新排序表行,那么您的模型   object有一个指定索引的属性。当用户移动时   一行,您相应地更新此属性。然而,这有   导致控制器注意到变化的副作用,等等   通知其代表更新(使用   控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath :)。   如果您只是使用“典型”中显示的此方法的实现   使用,“然后委托尝试更新表格视图。桌子   然而,由于这个原因,观点已经处于适当的状态   用户的行动。

     

因此,一般情况下,如果您支持用户驱动的更新,则应该   如果用户启动了移动,则设置标志。在实施中   您的委托方法,如果设置了标志,则绕过main方法   实现;例如:

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

    if (!changeIsUserDriven) {
        UITableView *tableView = self.tableView;
        // Implementation continues...

答案 4 :(得分:1)

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

桌子在移动时无法重新加载,在延迟后重新加载,你会没事的。

答案 5 :(得分:1)

对不起格雷格,我确信我做错了什么,但你回答对我不起作用。

虽然我的所有对象都能正确验证,但当我退出编辑模式时,其中一行会冻结(编辑控件不会消失),之后单元格也无法正常响应。

也许我的问题是我不知道如何使用你设置的移动属性(self.movi​​ng = YES)。你能澄清一下吗?非常感谢你。

豪尔赫

答案 6 :(得分:1)

这是实现此类功能的非常困难的方法。 这里可以找到更简单,更优雅的方式: UITableView Core Data reordering

答案 7 :(得分:1)

并[d&GT; isEditing]可用于确定是否启用了表的编辑。而不是像使用以下语句所建议的那样推迟它

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