【问题标题】:NSFetchedResultsController ignores fetchLimit?NSFetchedResultsController 忽略 fetchLimit?
【发布时间】:2011-06-18 23:56:11
【问题描述】:

我有一个 NSFetchedResultsController 来使用 Core Data 的内容更新 UITableView。这是非常标准的东西,我相信你们都看过很多次,但是我遇到了一些小问题。首先是我的代码:

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

 NSEntityDescription *entity = [NSEntityDescription entityForName:@"Article" inManagedObjectContext:self.managedObjectContext];

 [fetchRequest setEntity:entity];

 [fetchRequest setFetchLimit:20];

 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(folder.hidden == NO)"];
 [fetchRequest setPredicate:predicate];

 NSSortDescriptor *sort1 = [NSSortDescriptor sortDescriptorWithKey:@"sortDate" ascending:NO];
 [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sort1, nil]];

 NSFetchedResultsController *controller = [[NSFetchedResultsController alloc]
         initWithFetchRequest:fetchRequest
         managedObjectContext:self.managedObjectContext
         sectionNameKeyPath:nil
         cacheName:nil];
 [fetchRequest release];

 controller.delegate = self;

 self.fetchedResultsController = controller;

 [controller release];

 NSError *error = nil;
 [self.fetchedResultsController performFetch:&error];
 if (error) {
  // TODO send error notification
  NSLog(@"%@", [error localizedDescription]);
 }

问题是商店最初没有实体,因为它从网络服务下载和同步。发生的情况是 NSFetchedResultsController 用来自商店的 150 多行实体填充表,这就是 web 服务返回的数量。但是我设置了 20 的获取限制,这似乎被忽略了。但是,如果我关闭应用程序并使用商店中已有的数据重新开始,它就可以正常工作。我是我的代表,我这样做:

#pragma mark -
#pragma mark NSFetchedResultsControllerDelegate methods

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
 [self.tableView beginUpdates];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id  <NSFetchedResultsSectionInfo>)sectionInfo
 atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {

switch(type) {
    case NSFetchedResultsChangeInsert:
        [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeDelete:
        [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
        break;
    }
}


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

UITableView *tableView = self.tableView;

switch(type) {

    case NSFetchedResultsChangeInsert:
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeDelete:
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeUpdate:
        [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
        break;

    case NSFetchedResultsChangeMove:
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;
    }
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
 [self.tableView endUpdates]; 
}

这几乎是来自 Apple 开发文档的复制粘贴,有什么想法吗?

【问题讨论】:

    标签: iphone ios core-data ios4 nsfetchedresultscontroller


    【解决方案1】:

    这是我的伎俩:

    我在调用 NSManagedObjectContext 实例的“保存”方法后设置 NSFetchedResultsController 的委托。

    1. 在你的 UIViewController 上设置一个观察者并命名:例如。 “同步”
    2. 保存上下文后,使用该名称发布通知:“同步”并触发设置委托的函数(在您的视图控制器中)

    ps。如果您不再需要该观察者,请记住删除它

    【讨论】:

      【解决方案2】:

      我早在 2014 年就 iOS 6/7 向 Apple 提交了关于此问题的错误报告。正如许多其他人所指出的,它仍然是 iOS 9 和 10 上的一个错误。我最初的错误报告仍然开放,没有来自 Apple 的反馈。 Here is an OpenRadar copy of that bug report.

      这是我成功使用的修复程序,但它会被多次调用。谨慎使用。

      @objc func controllerDidChangeContent(controller: NSFetchedResultsController) {
          tableView.endUpdates() // Only needed if you're calling tableView.beginUpdates() in controllerWillChangeContent.
      
          if controller.fetchRequest.fetchLimit > 0 && controller.fetchRequest.fetchLimit < controller.fetchedObjects?.count {
                  controller.performFetch()
                  // Reload the table view section here
              }
          }
      }
      

      【讨论】:

        【解决方案3】:

        来自苹果文档:https://developer.apple.com/reference/coredata/nsfetchrequest/1506622-fetchlimit

        如果您设置了获取限制,框架会尽最大努力(但不保证)来提高效率。对于除 SQL 存储之外的每个对象存储,使用获取限制执行的获取请求实际上只是执行无限制获取并丢弃未请求的行。

        【讨论】:

          【解决方案4】:

          在某些情况下,这些仍然会崩溃,例如多次插入,或超出限制,... 您必须将所有更改保存到 4 个集合中,并在 -[UITableView endUpdates] 之前计算另外 4 个数组并删除/更新/插入到 tableView 中

          类似(假设只有一个部分):

          NSUInteger limit = controller.fetchRequest.fetchLimit;
          NSUInteger current = <current section objects count>;
          NSMutableArray *inserts = [NSMutableArray array];
          NSPredicate *predicate = [NSPredicate predicateWithFormat:@"row < %d", limit];
          
          if (insertedIndexPaths.count) {
              NSUInteger deletedCount = 0;
              for (NSIndexPath *indexPath in insertedIndexPaths) {
                  if (indexPath.row >= limit) continue;
                      current++;
                      if (current > limit) {
                          deletedCount++;
                          current--;
                          [deletedIndexPaths addObject:[NSIndexPath indexPathForRow:limit - deletedCount inSection:indexPath.section]];
                      }
                      [inserts addObject:indexPath];
              }
          }
          if (movedIndexPaths.count) {
              for (NSIndexPath *indexPath in movedIndexPaths) {
                  if (indexPath.row >= limit) {
                      [updatedIndexPaths addObject:[NSIndexPath indexPathForRow:limit - 1 inSection:indexPath.section]];
                  } else {
                      [inserts addObject:indexPath];
                  }
          }
          }
          [updatedIndexPaths minusSet:deletedIndexPaths];
          [deletedIndexPaths filterUsingPredicate:predicate];
          [updatedIndexPaths filterUsingPredicate:predicate];
          [_tableView insertRowsAtIndexPaths:inserts withRowAnimation:UITableViewRowAnimationFade];
          [_tableView reloadRowsAtIndexPaths:[updatedIndexPaths allObjects] withRowAnimation:UITableViewRowAnimationNone];
          [_tableView deleteRowsAtIndexPaths:[deletedIndexPaths allObjects] withRowAnimation:UITableViewRowAnimationFade];
          
          [_tableView endUpdates];
          deletedIndexPaths = nil;
          insertedIndexPaths = nil;
          updatedIndexPaths = nil;
          

          【讨论】:

            【解决方案5】:

            我知道这是一个老问题,但我有一个解决方案:

            由于NSFetchedResultsController 中存在一个不符合NSFetchRequestfetchlimit 的已知错误,因此您必须手动处理UITableViewDataSourceNSFetchedResultsControllerDelegate 方法中的记录限制。

            tableView:numberOfRowsInSection:

            - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
            
                id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
            
                NSInteger numRows = [sectionInfo numberOfObjects];
            
                if (numRows > self.fetchedResultsController.fetchRequest.fetchLimit) {
            
                    numRows = self.fetchedResultsController.fetchRequest.fetchLimit;
                }
            
                return numRows;
            }
            

            控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath:

            - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
            
                switch(type) {
            
                    case NSFetchedResultsChangeInsert:
            
                        if ([self.tableView numberOfRowsInSection:0] == self.fetchedResultsController.fetchRequest.fetchLimit) {
                            //Determining which row to delete depends on your sort descriptors
                            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:self.fetchedResultsController.fetchRequest.fetchLimit - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
            
                        }
            
                        [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                                        withRowAnimation:UITableViewRowAnimationFade];
                    break;
                    ...
                }
            }
            

            【讨论】:

            • 我最近在 iOS 7 上遇到了这个问题,我认为这是最好的解决方案,但我真的不明白 Apple 是如何解决这个问题的。我猜是时候报告错误了
            • 我似乎无法用上面的代码修复这个错误。我仍然得到一个异常和以下控制台日志:“CoreData:错误:严重的应用程序错误。在调用 -controllerDidChangeContent: 期间从 NSFetchedResultsController 的委托中捕获了一个异常:尝试将第 50 行插入第 0 节,但只有使用 userInfo (null) 更新后第 0 节中的 50 行"
            【解决方案6】:

            您遇到的问题是您在加载 fetchedResultsController 之前调用的是完整数据,因此它向您显示您需要做的一切就是加载所有信息,然后调用 fetchedResultsController

            例子

            - (void)viewDidLoad {
                [super viewDidLoad];
            
                // Loading Articles to CoreData
                [self loadArticle];
            }
            
            - (void)ArticleDidLoadSuccessfully:(NSNotification *)notification {
                NSError *error;
                if (![[self fetchedResultsController] performFetch:&error]) {
                    // Update to handle the error appropriately.
                    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
                    abort();  // Fail
                }
                [tableView reloadData];
            }   
            

            【讨论】:

            • 听起来不错,我会试一试,让你知道会发生什么。
            • 我想知道为什么人们仍然把 abort();在代码中。应用程序将因此被拒绝;-) 适当的错误处理就足够了(或者只使用 NSAssert)
            【解决方案7】:

            这是一个老问题,但我自己也遇到过(在 iOS 5 中)。我认为您遇到了此处描述的错误:https://devforums.apple.com/message/279576#279576

            该线程根据您是否有 sectionNameKeyPath 提供解决方案。因为我(像你一样)没有,所以答案是将 tableview 与 fetchedResultsController 分离。例如,不要使用它来确定行数:

            - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
            {
                    return [[[self.fetchedResultsController sections] objectAtIndex:0] numberOfObjects];
            

            只返回你所期望的:

                - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
            {
                    return fetchLimit;
            

            controller:didChangeObject 中,仅当 newIndexPath 在您的 fetchLimit 范围内时才插入新对象。

            【讨论】:

            • 这个 bug 在 iOS 6 或 7 中仍未修复。
            • 我可以确认它仍然存在于 iOS 8 中。
            • 显然 iOS 9 中也存在该错误
            • 这个问题在 iOS 10.2 中仍然存在。
            • iOS 11 中仍然存在
            猜你喜欢
            • 2012-02-19
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多