【问题标题】:UITableView unexpectedly bounces with beginUpdates()/endUpdates()/performBatchUpdates() using NSFetchedResultsController and CoreDataUITableView 使用 NSFetchedResultsController 和 CoreData 意外反弹 beginUpdates()/endUpdates()/performBatchUpdates()
【发布时间】:2018-06-22 05:55:55
【问题描述】:

当行数填满视图时,UITableView 使用 NSFetchedResultsController 和 CoreData 与 beginUpdates() / endUpdates() / performBatchUpdates() 意外反弹。 重现非常简单。 - 从主从应用程序模板(使用 CoreData)创建一个新项目。 - 在情节提要中,删除“showDetail”转场。 (我们不需要详细视图) - 在 MasterViewController 中,将 segue func prepare() 替换为:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {  
    let event = fetchedResultsController.object(at: indexPath)  
    let timestamp = event.timestamp  
    event.timestamp = timestamp // The idea is to simply update the Event entity.  
  }

启动应用程序(在 iOS 设备或模拟器中),并添加足够的行来填充视图(在 iPhone SE 中,它是 11 行)。 向下滚动视图,然后选择任意行。视图将迅速上下反弹。 这是一个错误,还是代码有问题?

【问题讨论】:

  • 所以你的意思是如果你注释掉didSelectRowAt 中的声明,那么它就不会反弹?但似乎您在 didSelectRowAt 中没有与 beginUpdate() endUpdate() n 相关的代码
  • 模板中没有didSelectRowAt 函数,我已经添加了它。是的,它会反弹。防止弹跳的唯一方法是删除controllerWillChangeContent()controllerDidChangeContent() 中的beginUpdates()endUpdates(),但我会失去它们的功能。您是否尝试过重现代码?
  • :还没试过,让我试试再回来
  • 所以我猜线程已经死了......

标签: ios swift uitableview core-data nsfetchedresultscontroller


【解决方案1】:

好的,我可能已经找到了解决方案,请告诉我你们的想法。 我们的想法是在performBatchUpdates 中处理insert/delete/move 并将update 排除在外。 所以我创建了这个枚举和属性:

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

controllerWillChangeContent 变为空:

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

didChange 变为:

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))
    }
  }

controllerDidChangeContent 变为:

  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)
  }

那你怎么看?

【讨论】:

  • 我现在也遇到了同样的问题 argg。你有没有找到更好的解决方案?
  • 我可能已经找到了解决方案。给我5分钟,我给你写代码。
  • 查看上面的优化解决方案。
【解决方案2】:

更精细的解决方案是

  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.
    }
  }

【讨论】:

  • 这太美了!谢谢,您为我节省了大量时间!
【解决方案3】:

我在UITableView unexpectedly bounces with beginUpdates()/endUpdates()/performBatchUpdates() 注意到了类似(重复?)的问题

我在那里添加了一个关于使用表格视图的estimatedHeightFor... 方法的答案。实现这些方法以返回正数可解决表格视图批量更新期间的奇数反弹问题。

【讨论】:

    【解决方案4】:

    这可能会有所帮助 -

    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()
                                }
    

    【讨论】:

    • 我应该把它放在哪里?在controllerWillChangeContent()controllerDidChangeContent() 中?
    • beginUpdates / endUpdates 在这种情况下根本没有效果。为什么表格视图是可选的?
    • 你有什么想法吗?您是否尝试重现该问题?
    • @nouatzi 无需重现该问题。开始/结束更新用于批量插入/删除操作,此代码甚至没有单个表操作。
    • 不,我正在修改 Xcode 模板代码。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多