UITableView исключение при изменении содержимого FRC - PullRequest
0 голосов
/ 09 июня 2018

У меня есть UITableView, который отображает данные из NSFetchedResultsController.В FRC имеется различное количество данных, варьирующееся от ~ 20 записей для одной выборки до ~ 14k записей для самой большой.Проблема, с которой я сталкиваюсь, заключается в том, что если я выполняю выборку, пока tableView прокручивает большую выборку, я получаю исключение.К тому времени, когда вызывается cellForRowAtIndexPath, FRC уже обновлен, и данных для него там нет, что приводит к исключению.

Я нашел этот пост , который звучит как то, что я 'Мы столкнулись, хотя я не могу решить ее с помощью этой методологии.

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID, for: indexPath) as! CustomTableViewCell
    // Exception occurs here
    let myObject = fetchedResultsController.object(at: indexPath)
    cell.myObject = myObject

    return cell
  }

Вот трассировка стека:

0   CoreFoundation                      0x0000000110b241e6 __exceptionPreprocess + 294
1   libobjc.A.dylib                     0x000000010b820031 objc_exception_throw + 48
2   CoreData                            0x000000010c2458fd -[NSFetchedResultsController objectAtIndexPath:] + 685
3   MyApp                         0x000000010aaf36c5 _T011MyApp0B14ViewControllerC05tableC0So07UITableC4CellCSo0fC0C_10Foundation9IndexPathV12cellForRowAttF + 437
4   MyApp                         0x000000010aaf39ec _T011MyApp0B14ViewControllerC05tableC0So07UITableC4CellCSo0fC0C_10Foundation9IndexPathV12cellForRowAttFTo + 92
5   UIKit                               0x000000010cf45567 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 783
6   UIKit                               0x000000010cf45ae4 -[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 74
7   UIKit                               0x000000010cf0ceaa -[UITableView _updateVisibleCellsNow:isRecursive:] + 3168
8   UIKit                               0x000000010cf2d7e0 -[UITableView layoutSubviews] + 176
9   UIKit                               0x000000010ceb77a8 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1515
10  QuartzCore                          0x000000010cc21456 -[CALayer layoutSublayers] + 177
11  QuartzCore                          0x000000010cc25667 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 395
12  QuartzCore                          0x000000010cbac0fb _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 343
13  QuartzCore                          0x000000010cbd979c _ZN2CA11Transaction6commitEv + 568
14  UIKit                               0x000000010cde22ef _UIApplicationFlushRunLoopCATransactionIfTooLate + 167
15  UIKit                               0x000000010d747662 __handleEventQueueInternal + 6875
16  CoreFoundation                      0x0000000110ac6bb1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
17  CoreFoundation                      0x0000000110aab4af __CFRunLoopDoSources0 + 271
18  CoreFoundation                      0x0000000110aaaa6f __CFRunLoopRun + 1263
19  CoreFoundation                      0x0000000110aaa30b CFRunLoopRunSpecific + 635
20  GraphicsServices                    0x0000000115c2fa73 GSEventRunModal + 62
21  UIKit                               0x000000010cde8057 UIApplicationMain + 159
22  MyApp                         0x000000010aacd427 main + 55
23  libdyld.dylib                       0x0000000111c5b955 start + 1

Я приветствую рекомендации по решению этой проблемы.Я бы не хотел делать что-то хакерское, например, отключать новые выборки, пока tableview не замедлится.Спасибо за прочтение.

Обновление

В ответ на комментарий, вот моя реализация UIFetchedResultsControllerDelegate.Это код, который Apple имеет в документации :

extension ViewController: NSFetchedResultsControllerDelegate {
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates()
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                  didChange sectionInfo: NSFetchedResultsSectionInfo,
                  atSectionIndex sectionIndex: Int,
                  for type: NSFetchedResultsChangeType) {
    switch type {
    case .insert:
      tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
    case .delete:
      tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
    case .move:
      break
    case .update:
      break
    }
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                  didChange anObject: Any,
                  at indexPath: IndexPath?,
                  for type: NSFetchedResultsChangeType,
                  newIndexPath: IndexPath?) {
    switch type {
    case .insert:
      tableView.insertRows(at: [newIndexPath!], with: .fade)
    case .delete:
      tableView.deleteRows(at: [indexPath!], with: .fade)
    case .update:
      tableView.reloadRows(at: [indexPath!], with: .fade)
    case .move:
      tableView.moveRow(at: indexPath!, to: newIndexPath!)
    }
  }

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
  }
}

Новая попытка

В ответ на комментарий я добавил свой NSFetchedResultsControllerDelegateреализация, которую Apple получила в документации.Я заметил, что опционные опции были развернуты принудительно, поэтому я добавил несколько операторов guard и отключил анимацию.Я получаю тот же сбой в том же месте, делая то же самое.

Вот обновленный метод делегата с добавленными защитными операторами и отключенной анимацией:

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                  didChange anObject: Any,
                  at indexPath: IndexPath?,
                  for type: NSFetchedResultsChangeType,
                  newIndexPath: IndexPath?) {

    guard let indexPath = indexPath else { return }

    switch type {
    case .insert:
      guard let newIndexPath = newIndexPath else { return }
      tableView.insertRows(at: [newIndexPath], with: .none)
    case .delete:
      tableView.deleteRows(at: [indexPath], with: .none)
    case .update:
      tableView.reloadRows(at: [indexPath], with: .none)
    case .move:
      guard let newIndexPath = newIndexPath else { return }
      tableView.moveRow(at: indexPath, to: newIndexPath)
    }
  }

Обновление 2

В ответ на комментарий это код, который я использую для обновления предиката.У меня есть наблюдатель startDate & endDate, который звонит updatePredicate().startDate и endDate обновляются при нажатии segmentedControl .:

  // This is used to set values for searching via segmentedControl
  @objc dynamic var startDate: Date?
  @objc dynamic var endDate: Date?

  func updatePredicate() {
    // you need a start date or stop executing
    guard let startDate = startDate else { return }

    var predicateArray: [NSPredicate] = []

    if let endDate = endDate {
      let startEndPredicate = NSPredicate(format: "time >= %@ AND time <= %@", argumentArray: [startDate, endDate])
      predicateArray.append(startEndPredicate)
    } else {
      // use the startDate's end of day if endDate is nil
      let startPredicate = NSPredicate(format: "time >= %@ AND time <= %@", argumentArray: [startDate, startDate.endOfDay!])
      predicateArray.append(startPredicate)
    }

    let sliderPredicate = NSPredicate(format: "magnitude >= %f", argumentArray: [magSlider.value])
    predicateArray.append(sliderPredicate)

    currentPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicateArray)

    fetchResults()
  }

Вот fetchResults ():

  func fetchResults() {
    do {
      fetchedResultsController.fetchRequest.fetchBatchSize = 35
      fetchedResultsController.fetchRequest.fetchLimit = 1_000
      try fetchedResultsController.performFetch()
    } catch let error {
      print("\(error) \(error.localizedDescription)")
    }
    DispatchQueue.main.async {
      self.tableView.reloadData()
      self.tableView.setContentOffset(.zero, animated: true)
      self.updateLabelsForResults()
    }
  }

Обновление 3

В ответ на другой комментарий ниже приводится декларация FRC:

  private lazy var fetchedResultsController: NSFetchedResultsController<EarthquakeEntity> = {
    let managedObjectContext = appDelegate.persistentContainer.viewContext
    let fetchRequest: NSFetchRequest<EarthquakeEntity> = EarthquakeEntity.fetchRequest()
    let dateSortDescriptor = NSSortDescriptor(key: #keyPath(EarthquakeEntity.time), ascending: false)
    fetchRequest.sortDescriptors = [dateSortDescriptor]
    let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: frcKeyPath, cacheName: nil)
    frc.delegate = self

    return frc
  }()

Ответы [ 2 ]

0 голосов
/ 18 июня 2018

Когда FRC используется вместе с fetchBatchSize, набор данных, на котором работает FRC, является прокси-массивом.Если данные для пакета еще не извлечены к моменту доступа к элементу пакета, они извлекаются во время доступа.

Потоковая безопасность массива прокси такая же, как и для управляемых объектов, которыевы работаете с ним - к нему нужно обращаться из частной очереди базового MOC.После этой статьи вы можете проверить, является ли проблема безопасности потока: https://oleb.net/blog/2014/06/core-data-concurrency-debugging/. В идеале доступ должен быть таким:

var myObject;
fetchedResultsController.managedObjectContext.performBlockAndWait({
    myObject = fetchedResultsController.object(at: indexPath) 
})
cell.myObject = myObject

Кроме того, во время сбоя проверьте, неэти утверждения верны:

1) fetchedResultsController.sections.first.numberOfObjects <= 
                                         fetchedResultsController.fetchRequest.fetchLimit

2) fetchedResultsController.sections.first.numberOfObjects == 
                                         tableView.numberOfRows(inSection: 0)

Если 1) неверно - FRC не соблюдает fetchLimit, и следует использовать только fetchBatchSize.

Если 2) нетtrue - это означает, что tableView не обновляется надлежащим образом при изменении набора данных FRC.Для определения причины потребуется дополнительная отладка.

0 голосов
/ 09 июня 2018

Я закончил играть с fetchLimit, и это, похоже, решило мою проблему.

Когда я выполняю выборку, я использую ее следующим образом:

fetchedResultsController.fetchRequest.fetchLimit = 2_500

2500 примернонастолько большой, насколько я мог получить, не разбивая приложение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...