UITableView在滑动删除后会冻结,但整个用户界面并未冻结

9

全屏表格视图iPad应用程序。我已经在我的行上启用了滑动删除。行动画始终在删除后完成(commitEditingStyle完成),但偶尔整个表视图会冻结。注意,不是整个UI,因此不是阻止主线程。我能够点击列标题或导航控制器上的返回按钮,但表本身被锁定并且无法滑动。我可以通过轻松点击其中一个列标题按钮来解除冻结。

enter image description here

我完全不知道是什么原因导致了冻结。我正在使用NSFetchedResultsController,这是我的委托代码。它相当基础 (更新: 现在不再是基础的了。使用分批处理方法):

// MARK: NSFetchedResultsController delegate methods

lazy var deletedSectionIndexes : NSMutableIndexSet = {
    return NSMutableIndexSet()
}()

lazy var insertedSectionIndexes : NSMutableIndexSet = {
    return NSMutableIndexSet()
}()

lazy var deletedRowIndexPaths : [NSIndexPath] = {
    return [NSIndexPath]()
}()

lazy var insertedRowIndexPaths : [NSIndexPath] = {
    return [NSIndexPath]()
}()

lazy var updatedRowIndexPaths : [NSIndexPath] = {
    return [NSIndexPath]()
}()


func controllerWillChangeContent(controller: NSFetchedResultsController) {

}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch(type) {

    case .Delete:
        if let indexPath = indexPath {
            self.deletedRowIndexPaths.appendDistinct(indexPath)
        }
    case .Update:
        if let indexPath = indexPath {
            self.updatedRowIndexPaths.appendDistinct(indexPath)
        }
    case .Insert:
        if let newIndexPath = newIndexPath {
            self.insertedRowIndexPaths.appendDistinct(newIndexPath)
        }
    case .Move:
        if let indexPath = indexPath, newIndexPath = newIndexPath {
            self.insertedRowIndexPaths.appendDistinct(newIndexPath)
            self.deletedRowIndexPaths.appendDistinct(indexPath)
        }
    }
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    switch(type) {

    case .Delete:
        self.deletedSectionIndexes.addIndex(sectionIndex)
    case .Insert:
        self.insertedSectionIndexes.addIndex(sectionIndex)
    default:
        break
    }
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    self.tableView.beginUpdates()
    self.tableView.insertSections(self.insertedSectionIndexes, withRowAnimation: .None)
    self.tableView.deleteSections(self.deletedSectionIndexes, withRowAnimation: .None)

    self.tableView.insertRowsAtIndexPaths(self.insertedRowIndexPaths, withRowAnimation: .None)
    self.tableView.deleteRowsAtIndexPaths(self.deletedRowIndexPaths, withRowAnimation: .None)
    self.tableView.reloadRowsAtIndexPaths(self.updatedRowIndexPaths, withRowAnimation: .None)
    self.tableView.endUpdates()

    self.insertedSectionIndexes.removeAllIndexes()
    self.deletedSectionIndexes.removeAllIndexes()
    self.deletedRowIndexPaths.removeAll()
    self.insertedRowIndexPaths.removeAll()
    self.updatedRowIndexPaths.removeAll()        
}

在didChangeObject代理方法中调用了delete,但从技术上讲,这不是真正的删除。我只是将属性设置为-1,然后通过NSMangagedObjectContext保存该元素 - 在此时,NSFRC似乎会执行正确的操作,即从使用此谓词获取的获取对象列表中将其删除:
NSPredicate(format: "account = %@ and quantity != -1", account)

其中account是一个有效的帐户管理对象。90%或更多的时间,该行可以顺利消失。只是偶尔在动画完成后,表格会以我所描述的方式冻结。它永远不会在删除按钮仍然显示时冻结,因此我知道它是在调用commitEditingStyle之后发生的。删除按钮没有自定义实现。它是UITableView默认的滑动删除实现。以下是我的commitEditingStyle方法:

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        if let frameboardItem = self.fetchedResultsController.objectAtIndexPath(indexPath) as? IRMFrameBoardItemMO {
            if frameboardItem.isNew {
                // If it's never been pushed to the server, just delete locally. This will trigger a table reload
                // via NSFetchedResultsController
                DataManager.mainContext.deleteObject(frameboardItem)
            } else {
                // Otherwise mark it with a negative quantity which tells the server to delete it and tells the
                // app to hide it.
                frameboardItem.quantity = -1
            }

            do {
                try DataManager.mainContext.save()
            } catch let error as NSError {
                dLog("Something went wrong: \(error.localizedDescription)")
            }

        }

    }

}

你可以在这里看到一个视频,它与我所说的内容有关。由于时长超过两分钟,你可能不想看完整个视频,但我会将其放在这里供参考。

https://vimeo.com/153406113

我很乐意听取任何建议。

更新

我已经更新了NSFRC代理方法,采用批处理方法以确保所有更新一次性应用。但这并没有解决问题。表格仍然会定期冻结。


你确定数据源正在正确更新吗?在以下语句中,是否存在frameboardItemnil的情况:if let frameboardItem = self.fetchedResultsController.objectAtIndexPath(indexPath) as? IRMFrameBoardItemMO?如果是nil,那么表格是否会在不更新数据源的情况下被更新? - Alexander
  1. tableView是否在子视图控制器中?
  2. 如果为了测试目的,您更改其中一个响应触摸的按钮,以便它触发TV滚动到顶部(或底部),它是否实际滚动?这可能会提示TV是仅忽略触摸还是在尝试滚动时冻结。
- pbasdf
@pbasdf 我不完全确定我理解了你的建议,但当表视图冻结时,我可以点击状态栏,表视图会像预期的那样滚动到顶部。然而,当尝试通过滑动来滚动时,它仍然被冻结。 - Matt Long
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - pbasdf
@pbasdf 不行。单元格上没有手势识别器。 - Matt Long
@MattLong 你是否为NSManagedObjectContextDidSaveNotification添加了观察者?如果是的话,能否展示一下上述观察者的选择器代码,并检查一下当你有多个NSManagedContexts时如何合并更改。 - Allen
4个回答

2

我对这个问题也有猜测。我的想法是,controllerDidChangeContent可能会被调用两次或更多次,并且比表格刷新更快,这导致多次调用tableView.beginUpdates(),从而使表格挂起。

因此,为了解决这个问题,我建议将更新包装在dispatch_async块中,或者只是一个简单的布尔标志。

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    dispatch_async(dispatch_get_main_queue(), { () -> Void in
         self.tableView.beginUpdates()
         // ..... rest of update code 
         self.updatedRowIndexPaths.removeAll()
    })
}

那么这到底在做什么呢?更新正在排队吗? - Matt Long
@MattLong 是的,更新正在队列中逐个执行。 - sage444
不幸的是,它并不完全是万能药,但它非常接近。我甚至使用了这种技术来复制冻结,但发生的频率非常少--几乎从未发生。我接受您的答案是正确的。感谢您的尝试。我很感激。 - Matt Long

0

你尝试过以更常见的方式实现NSFetchedResultsControllerDelegate的代理吗?我的意思是在fetchedResultController要求时开始表格更新,进行更新,然后结束更新。

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    self.tableView.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    /* update table here */
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    /* update table here */
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {    
    self.tableView.endUpdates()
}

更新:
当您“标记对象为已删除”时,是否可能会触发更复杂的对象更改链,从而导致多次调用didChangeObject函数? 您是否追踪过在单个“删除标记”期间调用didChangeObject函数的次数?


是的。在切换到批量更新之前就是这样的。有关详细信息请参见问题。 - Matt Long
@MattLong 是的,抱歉我错过了那个。 - Mykola Denysyuk

0

使用块。

我不清楚 MOC 是从哪个线程访问的,但由于您正在使用 fetchedResultsController,很可能是主线程。

因此,您可能希望执行

  • deleteObject
  • save

在一个 performBlockAndWait 中等待。这可能有助于保证数据完整性。代码示例:

DataManager.mainContext.performBlockAndWait { () -> Void in
    DataManager.mainContext.deleteObject(frameboardItem)
    if DataManager.mainContext.hasChanges {
        do {
            try DataManager.mainContext.save()
        } catch let error as NSError {
            dLog("Something went wrong: \(error.localizedDescription)")
        }
    }
}

0

我认为TableView冻结不是由于内存问题或不平衡的begin*/endEditing调用(会抛出异常或发送信号)。

我认为可能是在主线程以外的线程上执行操作。在这种情况下,即使使用块也无济于事。(设置断点并检查哪个线程停止...另外:在真实设备上进行测试)

我的想法是尝试一些不同的方法,例如将要添加或删除的数据添加到临时数组中,并在一个方法运行中更新TableView(在您的获取结果控制器结束其委托调用后,调用该方法显式地在主线程上运行)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接