移动部分中的最后一行时 UITableView endUpdates 崩溃

发布于 2024-12-13 13:50:36 字数 6131 浏览 0 评论 0原文

我有一个 UITableViewController,它由 NSFetchedResultsController 支持。

我的 NSFetchedResultsController 根据布尔值将结果分为两个部分。

在后台线程中,数据源会发生更改,以便添加或删除行。 我的后台线程的 NSManagedObjectContext 正确合并,这种情况在大多数情况下都可以正常工作。当数据更改时,行会发生变化,并通过动画在各部分之间移动。

然而,在一种情况下,我的应用程序因 EXC_BAD_ACCESS 崩溃。 这种情况是当一个部分中的最后一行移动到另一个部分时。 (下面的堆栈跟踪)。崩溃发生在 objc_msgSend 中,但正常调试提示我使用没有返回任何有用的东西,我只是收到“值无法转换为整数” gdb

NSFetchedResultsControllerDelegate 方法按以下顺序调用:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
     // ^ The parameters specify a deletion of the 1st section
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;
     // ^ The parameters specify a moved row from the 1st section to the 1st section 
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

浏览 StackOverflow,许多遇到类似问题的人都使用 NSArray 或 NSMutableArray 手动支持 UITableViewController,而他们只是没有在正确的时间更新数据源。在这种情况下,NSFetchedResultsController 正在处理返回行数和节数,并且在调用 [tableView endUpdates] 时肯定会更新。

有谁有任何调试技巧、指针或解决方案吗? 这篇博文提示在解决创建新部分的类似问题时。

如果我无法按原样解决此问题,我有两个选择:

  1. 放弃行上的动画,只需调用 [tableView reloadData]
  2. 切换到 UITableView 使用 NSArray 支持的存储并希望获得最佳

更新

我已将 Apple 示例核心数据应用程序“位置”修改为直接使用 NSFetchedResultsController 并重现了该问题。 我已上传此项目的源代码。使用该项目来重现该问题。

  1. 只需单击几次 + 按钮并等待
  2. 每 5 秒就有一个项目被移动到另一个类别。
  3. 当第一个类别即将清空时,它崩溃了。

崩溃有时是因为按照有关该主题的其他 StackOverflow 问题为同一单元格创建两个动画。更常见的崩溃是如上面和下面所讨论的。

参考

堆栈跟踪:

#0  0x31e0afbc in objc_msgSend ()
#1  0x32c11522 in -[_UITableViewUpdateSupport(Private) _computeRowUpdates] ()
#2  0x32c10510 in -[_UITableViewUpdateSupport initWithTableView:updateItems:oldRowData:newRowData:oldRowRange:newRowRange:context:] ()
#3  0x32c0f99e in -[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:] ()
#4  0x32c0e66c in -[UITableView endUpdatesWithContext:] ()
#5  0x000088d6 in -[DraftHistoryController controllerDidChangeContent:] (self=0x3f6f1e0, _cmd=0x377ca29c, controller=0x3f716e0) at /Users/ataylor/Documents/Documents/Programming/iPhone/Drafter/Drafter/Drafter/DraftHistoryController.m:323
#6  0x3775a892 in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] ()

我的 NSFetchedResultsControllerDelegate 方法与 Apple 示例代码中的方法相同:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    [self.tableView beginUpdates];
}


- (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];
            // Reloading the section inserts a new row and ensures that titles are updated appropriately.
            [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}


- (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)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    [self.tableView endUpdates];     // <---- Crash occurs here
}

相关的 UITableViewControllerDelegate 方法如下:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

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

I have an UITableViewController which is backed by an NSFetchedResultsController.

My NSFetchedResultsController put results into two sections based on a boolean.

In a background thread, the datasource is altered such that rows are added or removed.
I have my background thread's NSManagedObjectContext's merging correctly and this situation work fine for most cases. Rows alter when the data is changed and move between sections with animations.

There is one situation however where my application crashes with an EXC_BAD_ACCESS.
The case is when the last row in a section is moved to the other section. (Stack trace below). The crash occurs in objc_msgSend but the normal debugging tips I use aren't returning anything helpful, I simply receive "Value can't be converted to integer" from gdb.

The NSFetchedResultsControllerDelegate methods get called in the following order:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
     // ^ The parameters specify a deletion of the 1st section
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;
     // ^ The parameters specify a moved row from the 1st section to the 1st section 
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

Looking through StackOverflow, many of the people who encounter similar issues are backing their UITableViewController manually using an NSArray or NSMutableArray and they're simply not updating the datasource at the correct time. In this case the NSFetchedResultsController is handling returning the number of rows and sections and is definitely updated by the time the [tableView endUpdates] is called.

Has anyone got any debugging tips, pointers or solutions?
This blog post hints at working around a similar problem where a new section is created.

I have two options if I cannot solve this issue as is:

  1. Forgo animation on rows and simply call [tableView reloadData]
  2. Switch to using NSArray backed storage for the UITableView and hope for the best

Update

I have modified the Apple sample Core Data application "Locations" to use an NSFetchedResultsController directly and reproduced the issue.
I have uploaded the source code for this project. Use that project to reproduce the issue.

  1. Simply click the + button a few times and wait
  2. Every 5 seconds an item gets moved to another category.
  3. When the first category is about to be emptied, it crashes.

The crash is sometimes about creating two animations for the same cell as per other StackOverflow questions on the topic. More commonly the crash is as discussed above and below.

Reference

The stack trace:

#0  0x31e0afbc in objc_msgSend ()
#1  0x32c11522 in -[_UITableViewUpdateSupport(Private) _computeRowUpdates] ()
#2  0x32c10510 in -[_UITableViewUpdateSupport initWithTableView:updateItems:oldRowData:newRowData:oldRowRange:newRowRange:context:] ()
#3  0x32c0f99e in -[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:] ()
#4  0x32c0e66c in -[UITableView endUpdatesWithContext:] ()
#5  0x000088d6 in -[DraftHistoryController controllerDidChangeContent:] (self=0x3f6f1e0, _cmd=0x377ca29c, controller=0x3f716e0) at /Users/ataylor/Documents/Documents/Programming/iPhone/Drafter/Drafter/Drafter/DraftHistoryController.m:323
#6  0x3775a892 in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] ()

My NSFetchedResultsControllerDelegate methods are the same ones from the Apple sample code:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    [self.tableView beginUpdates];
}


- (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];
            // Reloading the section inserts a new row and ensures that titles are updated appropriately.
            [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}


- (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)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    [self.tableView endUpdates];     // <---- Crash occurs here
}

The relevant UITableViewControllerDelegate methods are as follows:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

超可爱的懒熊 2024-12-20 13:50:36

我快速看了一眼。您修改后的位置项目没有对我造成崩溃,但它确实生成了核心数据异常。问题在于重新加载controller:didChangeObject:中的部分。我将代码更改如下,在 iOS 4.3 和 iOS 5 上,一切看起来都不错(包括章节标题)。

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch(type) {

        // other cases here

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

I took a quick look. Your modified Locations project didn't crash on me but it did generate core data exceptions. The problem lies in reloading the sections in controller:didChangeObject:. I changed the code as follows and everything looks good to me (including the section titles) on both iOS 4.3 and iOS 5.

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch(type) {

        // other cases here

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
老旧海报 2024-12-20 13:50:36

我对通过添加新部分解决问题的代码非常好奇,因此我决定用它来代替我当前的 NSFetchedResultsControllerDelegate 方法,它确实解决了我的问题。追踪,我发现Apple“Locations”示例中的这段代码在使用NSFetchedResultsController时崩溃:

    case NSFetchedResultsChangeMove:
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        // Reloading the section inserts a new row and ensures that titles are updated appropriately.
        [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
        break;

但以下代码工作完美:

    case NSFetchedResultsChangeMove:
        if (newIndexPath != nil) {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation: UITableViewRowAnimationTop];
        }
        else {
            [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

我没有出现章节标题会更改的情况,我不会继续调试来解决苹果的代码旨在解决章节标题的问题。

I was super curious about the code that fixed an issue with adding a new section so I decided to pop it in place of my current NSFetchedResultsControllerDelegate methods and it indeed solved my problem. Tracing through, I have discovered that this code from the Apple "Locations" sample crashes when using the NSFetchedResultsController:

    case NSFetchedResultsChangeMove:
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        // Reloading the section inserts a new row and ensures that titles are updated appropriately.
        [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
        break;

But the following code works perfectly:

    case NSFetchedResultsChangeMove:
        if (newIndexPath != nil) {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation: UITableViewRowAnimationTop];
        }
        else {
            [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

I don't have a case where section headings will change, I won't continue debugging to solve the issue Apple's code purports to fix around section titles.

美煞众生 2024-12-20 13:50:36

因为每个问题都需要快速回答,所以这里有一个(基于 XJones 接受的答案)。与其他答案的情况一样,神奇之处在于 .Move:删除并插入而不是移动行。

这是我的 NSFRC 委托 didChangeObject:

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch type {
    case .Delete:
        if let deletePath = indexPath {
            self.tableView.deleteRowsAtIndexPaths([deletePath], withRowAnimation: .None)
        }
        break
    case .Insert:
        if let insertPath = newIndexPath {
            self.tableView.insertRowsAtIndexPaths([insertPath], withRowAnimation: .Fade)
        }
        break
    case .Update:
        if let updatePath = indexPath {
            self.tableView.reloadRowsAtIndexPaths([updatePath], withRowAnimation: .None)
        }
        break
    case .Move:
        if let indexPath = indexPath, newIndexPath = newIndexPath {
            self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
            self.tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
        }
        break
    }
}

Because every question needs a swift answer, here's one (based on XJones's accepted answer). As is the case with the other answers, the magic is in the .Move: delete and insert instead of moving the row.

This is my NSFRC delegate didChangeObject:

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch type {
    case .Delete:
        if let deletePath = indexPath {
            self.tableView.deleteRowsAtIndexPaths([deletePath], withRowAnimation: .None)
        }
        break
    case .Insert:
        if let insertPath = newIndexPath {
            self.tableView.insertRowsAtIndexPaths([insertPath], withRowAnimation: .Fade)
        }
        break
    case .Update:
        if let updatePath = indexPath {
            self.tableView.reloadRowsAtIndexPaths([updatePath], withRowAnimation: .None)
        }
        break
    case .Move:
        if let indexPath = indexPath, newIndexPath = newIndexPath {
            self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
            self.tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
        }
        break
    }
}
凤舞天涯 2024-12-20 13:50:36

我超时时崩溃了,我删除了一个部分中的最后一行。

请确保您已实现 controller:didChangeSection:atIndex:forChangeType: 来处理部分删除。

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

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

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

I was getting a crash overtime I deleted the last row in a section.

Do make sure you have implemented controller:didChangeSection:atIndex:forChangeType: to handle section removal.

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

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

        case NSFetchedResultsChangeDelete:
            [self.viewTable deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                          withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文