【问题标题】:Rearranging UITableView with Core Data [duplicate]使用核心数据重新排列 UITableView [重复]
【发布时间】:2010-11-12 11:39:06
【问题描述】:

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

我正在尝试找到一个代码示例,该示例显示了当单元格使用 fetchedResultsController(即与核心数据结合)时如何处理 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];
    }
}

【问题讨论】:

  • 我的解决方法有点不同(测试时会发布)。为此真正需要的是“Reverse-NSFetchedResultsController”。本质上为 NSFetchedResultsController 提供 UITableView、排序顺序和节标题,然后让它将所有内容写回 CoreData。我想我可以试着把它放在一起玩。
  • 绝对......比我的解决方案更优雅一点,它允许在存储的数据和表用户界面之间进行双向同步。我很想看看你完成后的想法。
  • 对于它的价值,Matt Long 最近发布了一个关于重新排序 NSFetchedResultsController 支持的表中的行的好教程:cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller
  • 查看这个简单的解决方案:stackoverflow.com/a/15625897/308315

标签: iphone uitableview core-data


【解决方案1】:

通常,当您看到这样的工件时,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];
    }

问题是您实际上并没有更改底层模型中的顺序。那些 indexPaths 来自 UITableViewController,它告诉你用户在这些到点之间拖动,你需要相应地更新底层数据。但 fetchedResultsController 始终按排序顺序排列,因此在您更改这些属性之前,什么都没有移动。

问题是,它们没有被移动,你被要求告诉你需要移动它们(通过调整 sortable 属性)。你真的需要更多类似的东西:

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

这将导致对象重新排序,然后检查并清理本应向上移动的其他对象的任何订单号,注意索引可能已移动。

【讨论】:

    【解决方案2】:

    以下是现在正式工作的内容,包括删除、移动和插入。每当有影响订单的编辑操作时,我都会“验证”订单。

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

    【讨论】:

    • 谢谢格雷格,上面的代码对我有用。我必须更改 controllerDidChangeContent 代码以添加到 endUpdates 以使删除和插入对 ui 更新起作用: if (self.movi​​ng) { self.movi​​ng = NO; [self.theTableView reloadData]; [self performSelector:@selector(save) withObject:nil afterDelay:0.02]; } else { [self.theTableView endUpdates]; }
    【解决方案3】:
    - (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];
    }
    

    桌子在移动时不能重新加载,延迟后再加载就可以了。

    【讨论】:

    • 这仍然带来了异常,请参阅我更新的“答案”了解现在的工作情况。
    • 很抱歉,我想知道发生了什么。我总是为此使用可变数组,这看起来不那么复杂,但并不能很好地为您的代码示例提供服务。 叹息
    • 你为什么不这样做呢[table performSelector:@selector(reloadData) withObject:nil afterDelay:0.02]; 这样你就不必另辟蹊径了:)
    • 这个是有效的,但效果似乎不好,整个视图将被刷新。看起来这个线程中的选择答案很好,但我认为似乎没有必要“插入”和“删除”,我只是使用字段“displayIndex”来控制顺序,如果一行移动到列表中的另一个位置,我只是使用冒泡排序方式更改字段值,即可以正常工作,不需要“插入”和“删除”
    【解决方案4】:

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

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

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

    豪尔赫

    【讨论】:

    • 我以前也见过同样的事情。不幸的是,我已经好几个星期没有挖掘我的资源了,我也不再把它们留在这里工作了。我会看看我是否可以追踪我如何使用 self.movi​​ng 但如果我记得的话,这是延迟更新核心数据存储/获取的结果,直到编辑/移动完成之后。在那之后,我确保“order”属性与它应该是一致的,然后更新核心数据存储/获取的结果,然后重新加载/刷新表中显示的数据,以便清除视觉上的不一致。这只是凭记忆,我会检查的。
    • 现在正在查看所有内容...初始化移动到 NO。当我们检测到 didObJectChange/NSFetchedResultsChangeMove 时,将移动设置为 YES 并 validateLinkOrder。不要在 controllerWillChangeContent 中做任何事情。但是在controllerDidChangeContent中,看看moving是否为YES,如果是,我们设置为NO,告诉tableViewreloadData,延迟0.02后执行“save”选择器。 “save”选择器基本上告诉 managedObjectContext 保存并记录任何错误,但如果出现问题,它会在 try/catch 异常处理中执行此操作。
    • 在上面的“已检查”答案中查看我修改后的代码清单。
    • 终于让它工作了。我的问题是在 order 属性中存储一个 int 而不是 NSNumber。那些 Core Data 崩溃非常丑陋,而且信息非常无用。非常感谢!!!
    【解决方案5】:

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

    【讨论】:

    • 好吧,如果您不使用 fetchedResultsController 进行重新排序,事情自然会变得很多简单。至少,在它的重新排序部分方面更简单。我认为如果 AppleDev 管理部门推荐使用 fetchedResultsController,那么我们应该有一种优雅的重新排列方式。不幸的是,看起来这两个东西虽然是兼容的,如上图,但它们肯定不是优雅的,如上图。
    【解决方案6】:

    [ isEditing] 可用于确定是否启用表格的编辑。而不是通过使用以下语句来延迟它的建议

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

    【讨论】:

      【解决方案7】:

      当您在表格视图中移动一行时,实际上是同时将一组其他行(至少包含一行)移动到另一个方向。诀窍是只更新此块和移动项目的 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);
          }
      }
      

      【讨论】:

        【解决方案8】:

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

        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 旨在响应 模型层的变化。如果您允许用户重新排序表行, 那么您的委托方法的实现必须考虑到这一点 帐户。

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

        因此,一般来说,如果您支持用户驱动的更新,您应该 如果用户发起移动,则设置一个标志。在实施中 你的委托方法,如果设置了标志,你绕过主要方法 实施;例如:

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

        【讨论】:

        • 这个forin things setValue... 非常简单,并为我节省了一堆试图提高效率但无法正常工作的代码......但是 - 它的效率有多低?假设我需要关心不到 200 个对象的效率吗?
        • @AvielGross 我认为响应用户操作一次更新 200 个对象不会是明显的开销。您可能需要注意的唯一部分是managedObjectContext save - 在您的目标最慢的设备上进行测试,如果您发现任何问题,您可能需要在后台线程上进行保存,或者推迟它。
        • 所以如果我的moc 是用NSMainQueueConcurrencyType 创建的,我可以在[context performBlock...] 中调用save: 方法吗?还是performBlockAndWait...?
        • @AvielGross 我认为我们现在正在偏离这个问题的主题;我建议提出一个新问题 - 但要回答,这些方法都无济于事,因为 NSMainQueueConcurrencyType 意味着它们将在主线程上运行该块,并且此代码已经在主线程上运行。 如果存在性能问题,您可能希望将保存到磁盘/闪存的内容移至后台线程,但多线程核心数据是一个大而棘手的话题。
        猜你喜欢
        • 2011-01-22
        • 1970-01-01
        • 1970-01-01
        • 2013-10-21
        • 1970-01-01
        • 1970-01-01
        • 2012-12-08
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多