UITableView 在滑动删除但不是整个后冻结 UI
UITableView Freezes After Swipe to Delete But Not Whole UI
Full-screen table 查看 iPad-only 应用程序。我已启用滑动以删除我的行。行动画总是在删除后完成(commitEditingStyle 完成),但有时整个 table 视图会冻结。请注意,不是整个 UI,所以它不是阻塞的主线程。我可以点击一列 header 或点击导航控制器上的后退按钮,但 table 本身会锁定并且无法滑动。我可以很简单地通过点击我的 header 栏中的一个按钮来解冻它。
对于可能导致冻结的原因,我完全不知所措。我正在使用 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 委托方法中调用的,但是,从技术上讲,这不是真正的删除。我只是将 属性 设置为 -1,然后通过 NSMangedObjectContext 保存该元素——此时 NSFRC 似乎做了正确的事情,即将它从获取的 objects 列表中删除使用此谓词获取:
NSPredicate(format: "account = %@ and quantity != -1", account)
其中 account
是一个有效的管理帐户 object。在 90% 或更多的时间里,该行会毫无问题地消失。只是偶尔,在动画完成后 table 冻结在我描述的庄园中。它永远不会冻结,删除按钮仍然显示,所以我知道它是在调用 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)")
}
}
}
}
您可以在此处观看我正在谈论的视频。两分钟多了,大家可能不想看全了,我放在这里供大家参考。
很想听听任何建议。
更新
我更新了 NSFRC 委托方法以使用批处理方法来确保一次性应用所有更新。这并没有解决问题。 table 仍然会定期冻结。
使用方块。
我不清楚 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)")
}
}
}
我认为 TableView 不会因内存问题或不平衡的 begin*/endEditing 调用而冻结(会抛出异常或发送信号)。
我认为它可能在主线程以外的线程上执行操作。在这种情况下,即使是块也无济于事。 (设置断点并检查哪个线程停止...,同时:在真实设备上测试)
我想解决这个问题,尝试一些不同的方法,比如将要添加或删除的数据添加到临时数组,并在一个方法中更新 TableView 运行(显式调用该方法 运行在您的获取结果控制器结束其委托调用后在主线程上。
这个问题我也有猜测。我的想法是 controllerDidChangeContent
可以被调用两次或更多次并且比 table 刷新更快,这导致多次调用 tableView.beginUpdates()
可以挂断 table.
所以为了解决这个问题,我建议在 dispatch_async
块中包装更新,或者只是简单的布尔标志
func controllerDidChangeContent(controller: NSFetchedResultsController) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.beginUpdates()
// ..... rest of update code
self.updatedRowIndexPaths.removeAll()
})
}
您是否尝试以更常见的方式实现 NSFetchedResultsControllerDelegate 的委托,我的意思是在 fetchedResultController 要求时开始 table 更新,进行更新然后结束更新?
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()
}
更新:
是否有可能,当您 'mark object as deleted' 时,它会触发一些更复杂的对象更改链,这反过来会导致 didChangeObject 函数被多次调用?
您是否跟踪过在单个 'deletion marking' 期间调用了多少次 didChangeObject 函数?
Full-screen table 查看 iPad-only 应用程序。我已启用滑动以删除我的行。行动画总是在删除后完成(commitEditingStyle 完成),但有时整个 table 视图会冻结。请注意,不是整个 UI,所以它不是阻塞的主线程。我可以点击一列 header 或点击导航控制器上的后退按钮,但 table 本身会锁定并且无法滑动。我可以很简单地通过点击我的 header 栏中的一个按钮来解冻它。
对于可能导致冻结的原因,我完全不知所措。我正在使用 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 委托方法中调用的,但是,从技术上讲,这不是真正的删除。我只是将 属性 设置为 -1,然后通过 NSMangedObjectContext 保存该元素——此时 NSFRC 似乎做了正确的事情,即将它从获取的 objects 列表中删除使用此谓词获取:
NSPredicate(format: "account = %@ and quantity != -1", account)
其中 account
是一个有效的管理帐户 object。在 90% 或更多的时间里,该行会毫无问题地消失。只是偶尔,在动画完成后 table 冻结在我描述的庄园中。它永远不会冻结,删除按钮仍然显示,所以我知道它是在调用 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)")
}
}
}
}
您可以在此处观看我正在谈论的视频。两分钟多了,大家可能不想看全了,我放在这里供大家参考。
很想听听任何建议。
更新
我更新了 NSFRC 委托方法以使用批处理方法来确保一次性应用所有更新。这并没有解决问题。 table 仍然会定期冻结。
使用方块。
我不清楚 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)")
}
}
}
我认为 TableView 不会因内存问题或不平衡的 begin*/endEditing 调用而冻结(会抛出异常或发送信号)。
我认为它可能在主线程以外的线程上执行操作。在这种情况下,即使是块也无济于事。 (设置断点并检查哪个线程停止...,同时:在真实设备上测试)
我想解决这个问题,尝试一些不同的方法,比如将要添加或删除的数据添加到临时数组,并在一个方法中更新 TableView 运行(显式调用该方法 运行在您的获取结果控制器结束其委托调用后在主线程上。
这个问题我也有猜测。我的想法是 controllerDidChangeContent
可以被调用两次或更多次并且比 table 刷新更快,这导致多次调用 tableView.beginUpdates()
可以挂断 table.
所以为了解决这个问题,我建议在 dispatch_async
块中包装更新,或者只是简单的布尔标志
func controllerDidChangeContent(controller: NSFetchedResultsController) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.beginUpdates()
// ..... rest of update code
self.updatedRowIndexPaths.removeAll()
})
}
您是否尝试以更常见的方式实现 NSFetchedResultsControllerDelegate 的委托,我的意思是在 fetchedResultController 要求时开始 table 更新,进行更新然后结束更新?
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()
}
更新:
是否有可能,当您 'mark object as deleted' 时,它会触发一些更复杂的对象更改链,这反过来会导致 didChangeObject 函数被多次调用?
您是否跟踪过在单个 'deletion marking' 期间调用了多少次 didChangeObject 函数?