在从 `NSFetchedResultsController` 后台更新期间避免跳入 `UITableView`

Avoid jump in `UITableView` during background update from `NSFetchedResultsController`

我有一个 UITableView 与书中的 NSFetchedResultsController 一起使用。

CoreData 对象由后台进程更新,因此每次更新都会自动触发 FetchedResultUITableView.

的更新

在这些更新期间下拉 UITableView 会导致令人不安的跳回顶部,然后在进一步拉动时快速回到下拉位置:

实体:

@objc(Entity)
class Entity: NSManagedObject {

    @NSManaged var name: String
    @NSManaged var lastUpdated: NSDate

}

结果:

private lazy var resultsController: NSFetchedResultsController = {

    let fetchRequest = NSFetchRequest( entityName: "Entity" )
    fetchRequest.sortDescriptors = [ NSSortDescriptor( key: "name", ascending: true ) ]
    fetchRequest.relationshipKeyPathsForPrefetching = [ "Entity" ]

    let controller = NSFetchedResultsController(
        fetchRequest: fetchRequest,
        managedObjectContext: self.mainContext,
        sectionNameKeyPath: nil,
        cacheName: nil
    )
    controller.delegate = self
    controller.performFetch( nil )

    return controller
}()

结合UITableViewResultsController的通常更新逻辑:

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

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

    switch( type ) {
    case NSFetchedResultsChangeType.Insert:
        tableView.insertRowsAtIndexPaths( [ newIndexPath! ], withRowAnimation: UITableViewRowAnimation.Fade )
    case NSFetchedResultsChangeType.Delete:
        tableView.deleteRowsAtIndexPaths( [ indexPath! ], withRowAnimation: UITableViewRowAnimation.Fade )
    case NSFetchedResultsChangeType.Update:
        tableView.reloadRowsAtIndexPaths( [ indexPath! ], withRowAnimation: UITableViewRowAnimation.None )
    case NSFetchedResultsChangeType.Move:
        tableView.moveRowAtIndexPath( indexPath!, toIndexPath: newIndexPath! )
    default:
        break
    }
}

后台更新(不是原来那个比较复杂)展​​示效果:

override func viewDidLoad() {
    super.viewDidLoad()

    //...

    let timer = NSTimer.scheduledTimerWithTimeInterval( 1.0, target: self, selector: "refresh", userInfo: nil, repeats: true)
    NSRunLoop.mainRunLoop().addTimer( timer, forMode: NSRunLoopCommonModes )
}

func refresh() {
    let request = NSFetchRequest( entityName: "Entity" )

    if let result = mainContext.executeFetchRequest( request, error: nil ) {
        for entity in result as [Entity] {
            entity.lastUpdated = NSDate()
        }
    }
}

我已经在 Whosebug 上搜索了几个小时,几乎尝试了所有方法。 唯一的想法是不调用 tableView.reloadRowsAtIndexPaths() 也不调用 beginUpdates() / endUpdates() 所以两者似乎都会造成这种影响。

但这不是选择,因为 UITableView 根本不会更新。

任何人有什么建议或解决方案吗?

你能做的很简单; 不要总是为核心数据更新重新加载 table,首先检查 table 是否静止..

当您需要从核心数据更新中更新 table 视图时,请检查 table 视图是否正在滚动(有很多方法可以这样做)。如果是这样,请不要依赖,但要这样做:在 table 视图中 class 标记 table 之后需要用布尔值更新,例如 "shouldUpdateTable"。 比在 "did finish scrolling animation" 的 table 视图委托方法中检查是否需要从该布尔标记重新加载 table,如果为真,则重新加载 table 并重置布尔值。

希望此方法有所帮助。

我刚刚找到适合我的解决方案。

有两个操作与我的后台更新冲突:

  1. dragging/scrolling
  2. 编辑(上面没有提到,但也需要)

因此,根据 Timur X 的建议,这两个操作都必须锁定更新。这就是我所做的:

多种原因的锁定机制

enum LockReason: Int {
    case scrolling = 0x01,
    editing   = 0x02
}

var locks: Int = 0

func setLock( reason: LockReason ) {
    locks |= reason.rawValue
    resultsController.delegate = nil
    NSLog( "lock set = \(locks)" )
}

func removeLock( reason: LockReason ) {
    locks &= ~reason.rawValue
    NSLog( "lock removed = \(locks)" )
    if 0 == locks {
        resultsController.delegate = self
    }
}

集成滚动锁定机制

override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    setLock( .scrolling )
}

override func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        removeLock( .scrolling )
    }
}

override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    removeLock( .scrolling )
}

处理编辑(在本例中为删除)

override func tableView(tableView: UITableView, willBeginEditingRowAtIndexPath indexPath: NSIndexPath) {
    setLock( .editing )
}

override func tableView(tableView: UITableView, didEndEditingRowAtIndexPath indexPath: NSIndexPath) {
    removeLock( .editing )
}

这是editing/delete

的整合
override func tableView(tableView: UITableView, titleForDeleteConfirmationButtonForRowAtIndexPath indexPath: NSIndexPath) -> String! {
    return "delete"
}

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == UITableViewCellEditingStyle.Delete {
        removeLock( .editing )
        if let entity = resultsController.objectAtIndexPath( indexPath ) as? Entity {
            mainContext.deleteObject( entity )
        }
    }
}