添加新对象后,NSFetchedResultsController fetchedObjects排序不正确

时间:2012-11-09 19:11:28

标签: ios uitableview nsfetchedresultscontroller

我有一个非常标准的应用程序,带有扩展CoreDataTableViewController的UITableViewController 来自CS193P stanford类(这只是UITableViewController实现的扩展                                 NSFetchedResultsControllerDelegate包含所有样板代码                                 来自NSFetchedResultsControllerDelegate文档)。

无论如何,我的表格显示了一个项目列表。这些项目有一个名为position的属性 整数(核心数据中的NSNumber)和NSFetchedResultsController设置为 NSSortDescriptor对位置进行排序。

这通常有效:当我的表打开时,performFetch完成并且项目在右边 订购。 我添加了一些日志消息来调试。

fetching Item with pedicate: parentList.guid == "123" and sort: (position, ascending, BLOCK(0x6bf1ca0))
fetched item: aaaaa has array:pos = 0 : 0
fetched item: bbbb has array:pos = 1 : 1
fetched item: cccc has array:pos = 2 : 2
fetched item: dddddd has array:pos = 3 : 3

第一行说的是执行了在GUID上使用谓词过滤的performFetch  以及在位置上排序的排序描述符。  从NSFetchedResultsController循环遍历fetchedObjects时,将记录下一行  取得之后。项目名称首先显示(aaaa,bbbb等)然后显示数组中的位置  fetchedObjects数组,然后是position属性的值。  你可以看到他们是如何排队的。

当我添加新项目,然后返回到父视图,然后转发到列表时出现问题  再次。新项目将添加正确的位置(结束)。但是当我回去转发时  几件物品都乱了。

起初我认为可能没有再次执行获取或者sortDescriptor丢失了  但是日志记录显示提取 正在发生,你可以看到事情发生故障。

 fetching Item with pedicate: parentList.guid == "123" and sort: (position, ascending, BLOCK(0x6bf1ca0))
 fetched item: bbbb has array:pos = 0 : 1     <= BAD
 fetched item: cccc has array:pos = 1 : 2     <= BAD
 fetched item: aaaaa has array:pos = 2 : 0    <= BAD
 fetched item: dddddd has array:pos = 3 : 3
 fetched item: eeee has array:pos = 4 : 4

请参阅:注意数组项0的位置是1,数组项2的位置是0,1是2!  因为这实际上是获取后立即获取的对象,  并且由于fetchRequestcontroller的sortDescriptor和谓词显然是正确的,如何  这甚至可能吗?

起初我认为它可能是表视图中的一个问题,但后来我添加了这个调试日志记录  在获取对象后,我知道它是获取的结果。

我还认为NSNumbers可能无法自动排序,所以我添加了自己的比较器  对整数值进行排序。但没有区别。

请注意,如果我再次前进和后退,下一次获取将以正确的顺序重新开始。  所有后续的提取也将如此。只有这一个在加载后才会发生。

有什么想法吗?

[UPDATE]

在评论中进行了一些有益的讨论后(感谢@MartinR和@tc感兴趣)我已经简化了一些事情并添加了一些代码来演示发生了什么。 简化:

  • 我现在正在排序项目&#34;标题&#34;因为它是一个简单的NSString。
  • 我不再使用子NSManagedObjectContext来创建新项目 - 它们直接在与列表相同的MOC中创建并立即保存(并同步)

添加一些代码来演示: 基本设置是项目列表的列表。标准Todo-app的东西。所以我的CoreData模型包含列表和项目。每个列表都有一组项目(1-many),每个项目都有一个返回其父列表的引用。

用户界面是2个TVC:ListOfListsTVC,点击列表名称并将其转移到ListOfItemsTVC

现在,由于项目列表用于不同的列表,因此每次设置新列表时,它都会设置一个全新的FRC。这发生在这里:

- (void)setupFetchedResultsController
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"item"];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"parentList.guid = %@", self.list.guid];
    request.predicate = predicate;


    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request                                                                    managedObjectContext:self.list.managedObjectContext sectionNameKeyPath:nil
                                                cacheName:nil];
    self.debug = YES;

}    

self.fetchedResultsController调用CoreDataTableViewController超类,这是从cs193p Stanford课程中逐字记录的:

- (void)setFetchedResultsController:(NSFetchedResultsController *)newfrc
{
self.debug = YES;
NSFetchedResultsController *oldfrc = _fetchedResultsController;
if (newfrc != oldfrc) {
    _fetchedResultsController = newfrc;
    newfrc.delegate = self;
    if ((!self.title || [self.title isEqualToString:oldfrc.fetchRequest.entity.name]) && (!self.navigationController || !self.navigationItem.title)) {
        self.title = newfrc.fetchRequest.entity.name;
    }
    if (newfrc) {
        if (self.debug) NSLog(@"[%@ %@] %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), oldfrc ? @"updated" : @"set");
        [self performFetch]; 
    } else {
        if (self.debug) NSLog(@"[%@ %@] reset to nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
        [self.tableView reloadData];
    }
}
}

它主要是调试信息,但其中的关键语句是:1。设置属性; 2.将FRC的delegate设置为此TVC(自我)和3.立即获取。

performFetch位于同一个CoreDataTableViewController类中,并转储我上面列出的所有调试信息:项目名称,fetchedObjects数组中的位置以及位置值。 (它实际上也是一个获取列表的通用方法,但是我为Item类进行了测试以获得额外的调试信息) 我不会在这里列出,但主要陈述是:

NSError *error;
[self.fetchedResultsController performFetch:&error];

// Debug:
NSArray *obs = [self.fetchedResultsController fetchedObjects];
// log all the debug info about the items in the fetched array to prove they're not sorted
// ...


[self.tableView reloadData];

所以基本上,获取并重新加载表。

当我第一次启动应用程序时,如果数据中包含项目列表,这似乎有效。在我添加新项目之后它似乎失败的地方。我在一个名为NewItemTVC的单独的静态TVC中执行此操作,而不是委托,我在块中使用回调来保存项目。但效果是一样的:它都是同步的。这是我在ListOfItemsTCV中保存的块

newItemTVC.saveCancelBlock2 = ^ (BOOL save, NSDictionary *descriptor) {
    if (save) {

        NSManagedObjectContext *moc = self.fetchedResultsController.managedObjectContext;
        Item *newItem = [Item itemWithDescriptor:itemDescriptor inManagedObjectContext:moc];

        // here's where I set the position but ignore this for now because
        // I'm sorting on "title" to debug and it has the same problem
        NSInteger newPosition = self.list.lastPosition + 1;
        newItem.position = [NSNumber numberWithInteger:newPosition];

        // and finally add it to the list
        [self.list addItemsObject:newItem];

        NSError *error = nil;
        BOOL saved = [moc save:&error];
        if (!saved) {
            NSLog(@"Unresolved error saving after adding item to parent %@, %@", error, [error userInfo]);
        }
    }

现在,在我保存项目之后,NewItemTVC弹出并重新加载ListOfItems,执行获取,有时具有正确的顺序,通常不会。在这种情况下,在viewWillAppear中执行提取。 (它不常以为但是我在调​​试时添加了这个。现在viewWillDisappear将委托设置为nil,并且弹出NewItemTVC会导致此代码在将FRC的委托设置回TVC后执行新的提取) 另请注意,从列表列表中 forward 时,这不会设置委托或执行提取,因为设置list属性已经这样做(设置委托并执行提取)。 所以实际上,从弹出NewItemTVC并在viewWillAppear中执行fetch返回是第一个排序错误的实例。

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    if (self.fetchedResultsController != nil && self.fetchedResultsController.delegate !=  self) {
        self.fetchedResultsController.delegate = self;
        [self performFetch];
        [self.tableView reloadData];
  }
}

真正出错的地方是当我点击返回查看我的ListOfListsTVC,然后再次点击列表返回到相同的ListOfItemsTVC(如果我有1个列表或打打不重要) 。我第一次这样做的项目总是乱序。有时候我可以重复4到5次,而且他们仍然会出现故障,但最终经过多次的后退和后退后,他们会按顺序继续保持这种状态。

现在,我已经使用&#34;标题&#34;(#shenized)调试信息了。我的物品,而不是位置。

[808:fb03] [ListOfItemsTVC performFetch] fetching Item with pedicate: parentList.guid ==    "DD1E1F25-BFC9-46B9-A637-109C0D6F0D1D" and sort: (title, ascending, compare:)
[808:fb03] fetched item: ccccc in array at index 0 
[808:fb03] fetched item: aaaaa in arrat at index 1
[808:fb03] fetched item: bbbbb in array at index 2

一些后退之后,它会落到aaaa,bbbb,ccccc - 正确的顺序。 在我看来,排序刚刚破裂或FRC是。

3 个答案:

答案 0 :(得分:0)

我在这里发布了我的“解决方案”,但我暂时不接受这个回答,以防万一我使用NSFetchedResultController导致排序是错的,而不只是一个错误。 (见问题下的评论)

最后,我通过获取所有获取的对象并在我想要的位置查找它来作弊。 这对所有情况都不起作用:对于我来说这很容易,因为我正在对整数进行排序,我知道列表是最新的(它没有在后台填充)而且很小,所以没有大的性能查看所有对象的问题。

具体来说:我替换了cellForRowAtIndexPath中的典型行,如下所示:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ....
    Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
    ...

用这个

{
    ...
    Item *item = [self _lookupItemAtIndexPath:indexPath];
    ...

这是在这里实施的

- (Item *)_lookupItemAtIndexPath:(NSIndexPath *)indexPath
{
    // Since the FRC is sorted on position, _theoretically_ this next call should just
    // return the right item, but the damn sorting doesnt always work (esp not after
    // a new addition), so we check if it's the one we expect and look it up if not
    Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];

    if (item.position.integerValue != indexPath.row) {
        // the sorting failed! so do a lookup to find the item with the right position
        // (Note that its crucial that the positions remain contiguous and up-to-date)
        NSArray *items = self.fetchedResultsController.fetchedObjects;
        NSUInteger index = [items indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
            if (((Item *)obj).position.integerValue == indexPath.row) {
                // found it - stop looking and return YES
                *stop = YES;
                return YES;
            }
            else {
                return NO;
            }
        }];

        NSAssert1(index != NSNotFound, @"Failed to find the item with the right position: %d", indexPath.row); 

        item = (Item *)[items objectAtIndex:index];

        // Temporary log for debugging: tells me how often the sort is actually failing
        NSLog(@"Warning: Item %@ was out of place, found it at %d instead of %d", item.title, index, indexPath.row);
    }
    return item;
}

请注意,我首先尝试原始查找并测试其是否正确,因此如果不必要,我不会进行“慢速”查找。大多数时候它 _un_necessary - 在添加内容之后它似乎只是一个问题。 (我可以从那里的NSLog消息中看出来)

同样,对于排序失败,这实际上不是回答 - 只是我的具体案例的解决方法 - 所以我不会将其作为答案(但是)

答案 1 :(得分:0)

我对CS193P示例代码有同样的问题。我的解决方案是保存父上下文(在对象创建/修改之后):

NSError* err;
if ([txt.managedObjectContext save:&err]) {
    if ([txt.managedObjectContext.parentContext hasChanges]) {
        if ([txt.managedObjectContext.parentContext save:&err]) {
            NSLog(@"parent context save ok");
        } else {
            NSLog(@"can't save parent context; error: %@", err);
        }
    }
} else {
    NSLog(@"can't save text; error: %@", err);
}

如果您使用新的UIDocument方法而不是旧的核心数据模板,并且手动设置NSManagedObjectContextNSPersistentStoreCoordinatorNSManagedObjectModel,则会显示“错误”。< / p>

答案 2 :(得分:0)

好的,所以我做了一些工具。我试图在虚拟项目中重现问题而不能。我想这可能意味着我的代码中存在错误,而不是核心数据。它太难以找到了。

因为我知道实际保存更新后问题“消失”,所以我研究了如何强制保存。 Apple文档说您不应该保存NSManagedObjectContext或其父级,因为“您回避了文档执行的其他重要操作”。但是,这是我发现强制保存的唯一方法(仅保存moc或使用updateChangeCount实际上不会导致SQL立即执行)。我不想赞成zxcat的响应,因为我不知道什么是“回避”,但它似乎有用。

更新:一夜之间我已经进一步思考并决定无论我做什么“错误”,NSFetchedResultsController中仍然存在错误。这是因为它可能最终处于不合逻辑的状态 - 即。 fetchedObjects的内容与sections的内容不符。在所有情况下,fetchedObjects始终是正确的。在某些情况下,像我和OP一样,sections反映了当前的持久状态,而不是内存状态。