2

我有一个非常标准的应用程序,它带有一个 UITableViewController,它从 CS193P stanford 类扩展了 CoreDataTableViewController(它只是 UITableViewController 的一个扩展,它实现了 NSFetchedResultsControllerDelegate 以及 NSFetchedResultsControllerDelegate 文档中的所有样板代码)。

无论如何,我的表格显示了一个项目列表。这些项目有一个称为position整数的属性(Core Data 中的 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 上的谓词过滤和位置上的排序描述符排序。在 fetch 之后从 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!由于这实际上是在 fetch 之后立即获取的对象,并且由于 fetchRequestcontroller 的 sortDescriptor 和 predicate 显然是正确的,这怎么可能呢?

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

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

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

有任何想法吗?

[更新]

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

  • 我现在对项目“标题”进行排序,因为它是一个简单的 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调用了来自 cs193p 斯坦福课程的 CoreDataTableViewController 超类:

- (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. 将delegateFRC 设置为此 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 后进行新的提取)另请注意从列表列表中前进时,这不会设置委托或执行提取,因为设置列表属性已经做到了(设置委托并执行提取)。所以事实上,从弹出 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 是。

4

3 回答 3

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 消息中看出这一点)

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

于 2012-11-15T20:01:22.350 回答
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方法而不是旧的 Core Data 模板手动设置NSManagedObjectContext,NSPersistentStoreCoordinatorNSManagedObjectModel.

于 2012-12-28T13:21:29.367 回答
0

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

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

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

于 2013-03-25T08:48:56.197 回答