【问题标题】:NSFetchedResultsController fetchedObjects are sorted incorrectly after adding a new objectNSFetchedResultsController fetchedObjects 添加新对象后排序错误
【发布时间】:2012-10-30 03:31:54
【问题描述】:

我有一个非常标准的应用程序,它带有一个扩展 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

第一行说的是 performFetch 发生在 GUID 上的谓词过滤 以及按位置排序的排序描述符。 当从 NSFetchedResultsController 循环遍历 fetchedObjects 时会记录下一行 取回之后。首先显示项目名称(aaaa、bbbb 等),然后是数组中的位置 fetchedObjects 数组,然后是 position 属性的值。 您可以看到它们是如何排列的。

当我添加一个新项目,然后回到父视图,然后转发到列表时,麻烦就来了 再次。新项目被添加到正确的位置(结束)。但是当我来回前进时 有几件物品有问题。

起初我以为可能没有再次执行 fetch 或者 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 和谓词显然是正确的,如何 这甚至可能吗?

起初我认为这可能是表格视图中的问题,但后来我添加了这个调试日志 在 fetchObjects 之后,所以我知道这是 fetch 的结果。

我也考虑到可能 NSNumbers 不能自动排序,所以我添加了自己的比较器 对整数值进行排序。但没有区别。

请注意,如果我再次来回前进,下一次 fetch 会将内容按正确的顺序放回原处。 所有后续提取也是如此。加载后只会发生这种情况。

有什么想法吗?

[更新]

在 cmets 中进行了一些有益的讨论(感谢 @MartinR 和 @tc 的关注)后,我稍微简化了一些事情并添加了一些代码来演示正在发生的事情。 简化:

  • 我现在对项目“title”进行排序,因为它是一个简单的 NSString。
  • 我不再使用子 NSManagedObjectContext 来创建新项目 - 它们直接在与列表相同的 MOC 中创建并立即(同步)保存

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

UI 是 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 斯坦福课程的逐字记录:

- (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 (self) 和 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 时,这不会设置委托或执行提取,因为设置列表属性已经这样做了(设置委托并执行提取)。 所以事实上,从弹出 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 次,它们仍然会出现故障,但最终经过多次来回后,它们会恢复正常并保持这种状态。

现在我使用的是项目的“标题”而不是位置,这是(已清理的)调试信息。

[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 坏了。

【问题讨论】:

  • 您确定您的委托已设置吗?不完全确定我是否在跟踪您的示例运行,但如果没有一组并实施,则 fetch 控制器将不会跟踪更改。
  • 请注意,您不能在(基于 SQlite)核心数据获取请求中使用基于块的排序描述符。 - 也许你可以展示你的代码来创建排序描述符、获取请求和获取结果控制器。
  • @BenZotto 代表肯定已经设置好了。为了尝试解决这个问题,我将其更改为取消设置并分别在 viewWillDisappear/viewWillAppear 上重新设置,但这没有区别。请注意,新创建和保存的项目确实出现在列表中的正确位置。问题仅在我返回然后再次前进以重新显示列表时才开始
  • @tc 当用户点击保存时,新项目 TVC 关闭后,保存发生在 TVC 的一个块中。 IE。我有一个静态 TVC,用于创建新项目,该项目使用“保存”按钮以模态方式显示,按下它会调用父 TVC 上的一个块,该块会保存,然后弹出 TVC。标准的东西。值得注意的是,我正在为我的新项目使用第二个临时 MOC。当我保存它时,我保存临时 MOC,然后父 MOC,然后我设置位置并再次保存父 MOC。
  • @MartinR 很奇怪。为什么不允许阻止?碰巧我确实使用了一个块来尝试调试问题 - 但这是为了以防 CoreData 无法自然地对 NSNumber 列进行排序。我很确定它可以。 (每次都有效,但第一次)

标签: ios uitableview nsfetchedresultscontroller


【解决方案1】:

我在这里发布我的“解决方案”,但我暂时不会接受这个作为答案,以防我使用 NSFetchedResultController 导致排序是错误的,而不仅仅是一个错误。 (见问题下的cmets)

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

具体来说:我替换了 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 消息中看出)

再一次,这并不是排序失败的真正答案 - 只是我针对特定情况的解决方法 - 所以我没有将其作为答案进行检查(还)

【讨论】:

  • 我也有类似的问题,我想你会发现它变得更糟了!如果您删除一个项目,您可能会在cellForRowAtIndexPath 中崩溃。假设您在一个部分中有 5 个项目,而您删除了一个 - 在 tableView 中一切正常。但是然后你回到更早的 vc,并再次转发到这个 tableView 并且它崩溃了。即使 frc 已获取 4 条记录,frc 中 sectionsnumberOfRows 方法仍然返回 5。当 UIManagedDocument 自动保存时,它会自行修复。我仍在努力解决这个问题,但不能做你所拥有的,因为我有部分。
【解决方案2】:

我在使用 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 的旧Core Data 模板,则会出现“错误”。

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

    • 啊,UIManagedDocument — 现在我有充分的理由继续避免它!我认为根本问题是NSFetchRequest 通过 SQL 获取对象顺序(以及因此部分信息)以提高效率,这不会反映内存中的更改。听起来正确的保存方法是-saveToURL:forSaveOperation:completionHandler:(这可能会隐式保存子上下文)。听起来UIManagedDocument 也使用 Core Data 来持久化对象图而不是数据库。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多