NSFetchedResultsController ignores fetchLimit?

2020-02-19 03:10发布

I have a NSFetchedResultsController to update a UITableView with content from Core Data. It's pretty standard stuff I'm sure you've all seen many times however I am running into slight problem. First here's my code:

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

The problem is that initially the store has no entities as it downloads and syncs from a webservice. What happens is that the NSFetchedResultsController fills the table with over 150 rows of entities from the store, which is how many the webservice returns. But I am setting a fetch limit of 20 which it appears to be ignoring. However, if I close out the app and start again with data already in the store, it works fine. Im my delegate i do this:

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

Which is pretty much copy-paste from Apple's dev documents, any ideas what's goin on?

7条回答
啃猪蹄的小仙女
2楼-- · 2020-02-19 03:22

I filed a bug report with Apple back in 2014 on iOS 6/7 about this issue. As many others have noted, it's still a bug on iOS 9 and 10. My original bug report is still open too with no feedback from Apple. Here is an OpenRadar copy of that bug report.

Here's a fix I've used with success but it will get called multiple times. Use with caution.

@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
        }
    }
}
查看更多
Luminary・发光体
3楼-- · 2020-02-19 03:24

These will still crash in some situations, like several inserts, or move over limit,... You have to save all the changes to 4 sets, and calculate another 4 arrays and delete/update/insert to tableView before -[UITableView endUpdates]

Some thing like (assume there is only one section):

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;
查看更多
放荡不羁爱自由
4楼-- · 2020-02-19 03:26

I know this is an old question, but I have a solution for it:

Since there is a known bug in NSFetchedResultsController that doesn't honor the fetchlimit of the NSFetchRequest, you have to manually handle the limiting of records within your UITableViewDataSource and NSFetchedResultsControllerDelegate methods.

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

controller: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;
        ...
    }
}
查看更多
再贱就再见
5楼-- · 2020-02-19 03:30

The problem you have is that you are calling before loading fetchedResultsController charge the full data so it shows you everything you need to do is load all the information and then call fetchedResultsController

Example

- (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];
}   
查看更多
Melony?
6楼-- · 2020-02-19 03:33

This is an old question but I just ran into it myself (in iOS 5). I think you're running into the bug described here: https://devforums.apple.com/message/279576#279576.

That thread provides solutions based on whether you have a sectionNameKeyPath or not. Since I (like you) didn't, the answer is to decouple the tableview from the fetchedResultsController. For example, instead of using it to determine the number of rows:

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

just return what you expect:

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

And in controller:didChangeObject, only insert the new object if the newIndexPath is within your fetchLimit.

查看更多
▲ chillily
7楼-- · 2020-02-19 03:35

This is my trick:

I set the NSFetchedResultsController's delegate after 'save' method on the NSManagedObjectContext instance is called.

  1. Set an observer on your UIViewController with a name: eg. 'Sync'
  2. after saving your context, post a notification with that name: 'Sync' and trigger a function (in your viewcontroller) that set the delegate

ps. remember to remove that observer if you don't need it anymore

查看更多
登录 后发表回答