UITableView unexpectedly bounces with beginUpdates()/endUpdates()/performBatchUpdates() using NSFetchedResultsController and CoreData

偶尔善良 提交于 2019-12-05 22:33:50

Ok, I might have found a solution, please tell me guys what you think. The idea would be to process insert/delete/move in performBatchUpdates and leave update out of it. So I've created this enum and property:

enum FetchedResultsChange<Object> {
  case insert(IndexPath)
  case delete(IndexPath)
  case move(IndexPath, IndexPath, Object)
}
var fetchedResultsChanges: [FetchedResultsChange<Event>] = []

And controllerWillChangeContent becomes empty:

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {}

didChange becomes:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .insert:
      self.fetchedResultsChanges.append(.insert(newIndexPath!))
    case .delete:
      self.fetchedResultsChanges.append(.delete(indexPath!))
    case .update:
      configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! Event) // So this stays untouched.
    case .move:
      self.fetchedResultsChanges.append(.move(indexPath!, newIndexPath!, anObject as! Event))
    }
  }

And controllerDidChangeContent becomes:

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    guard self.fetchedResultsChanges.count > 0 else { return }

    tableView.performBatchUpdates({
      repeat {
        let change = self.fetchedResultsChanges.removeFirst()
        switch change {
        case .insert(let newIndexPath):
          tableView.insertRows(at: [newIndexPath], with: .fade)
        case .delete(let indexPath):
          tableView.deleteRows(at: [indexPath], with: .fade)
        case .move(let indexPath, let newIndexPath, let event):
          configureCell(tableView.cellForRow(at: indexPath)!, withEvent: event)
          tableView.moveRow(at: indexPath, to: newIndexPath)
        }
      } while self.fetchedResultsChanges.count > 0
    }, completion: nil)
  }

So what do you think ?

I noticed the similar (duplicate?) question at UITableView unexpectedly bounces with beginUpdates()/endUpdates()/performBatchUpdates()

I added an answer there about using the estimatedHeightFor... methods of the table view. Implementing these methods to return a positive number fixes the odd bounce problem during table view batch updates.

A more refined solution is

  lazy var sectionChanges = [() -> Void]()
  lazy var objectChanges = [() -> Void]()

  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    guard controller == self._fetchedResultsController else { return }
    self.sectionChanges.removeAll()
    self.objectChanges.removeAll()
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
    guard controller == self._fetchedResultsController else { return }
    let sections = IndexSet(integer: sectionIndex)
    self.sectionChanges.append { [unowned self] in
      switch type {
      case .insert: self.tableView.insertSections(sections, with: .fade)
      case .delete: self.tableView.deleteSections(sections, with: .fade)
      default: break
      }
    }
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    guard controller == self._fetchedResultsController else { return }
    switch type {
    case .insert:
      if let verifiedNewIndexPath = newIndexPath {
        self.objectChanges.append { [unowned self] in
          self.tableView.insertRows(at: [verifiedNewIndexPath], with: .fade)
        }
      }
    case .delete:
      if let verifiedIndexPath = indexPath {
        self.objectChanges.append { [unowned self] in
          self.tableView.deleteRows(at: [verifiedIndexPath], with: .fade)
        }
      }
    case .update:
      if let verifiedIndexPath = indexPath, let event = anObject as? Event, let cell = self.tableView.cellForRow(at: verifiedIndexPath) {
        self.configureCell(cell, withEvent: event)
      }
    case .move:
      if let verifiedIndexPath = indexPath, let verifiedNewIndexPath = newIndexPath, let event = anObject as? Event, let cell = self.tableView.cellForRow(at: verifiedIndexPath) {
        self.configureCell(cell, withEvent: event)
        self.objectChanges.append { [unowned self] in
          self.tableView.moveRow(at: verifiedIndexPath, to: verifiedNewIndexPath)
        }
      }
    default: break
    }
  }

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    guard controller == self._fetchedResultsController else { return }
    guard self.objectChanges.count > 0 || self.sectionChanges.count > 0 else { return }
    self.tableView.performBatchUpdates({[weak self] in
      self?.objectChanges.forEach { $0() }
      self?.sectionChanges.forEach { $0() }
    }) { (finished) in
      // here I check if the tableView is empty. If so, I usually add a label saying "no item, click add button to add items."
      // If not, then I remove this label.
    }
  }

This may help -

UIView.performWithoutAnimation {
                                self.tableView?.beginUpdates()
                                let contentOffset = self.tableView?.contentOffset
                                self.tableView?.reloadRows(at: [IndexPath(row: j, section: 0)], with: .automatic)
                                self.tableView?.setContentOffset(contentOffset!, animated: false)
                                self.tableView?.endUpdates()
                            }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!