Search code examples
iosswiftuitableviewcore-datansfetchedresultscontroller

Bug with FetchedResultsController after setting it to nil in viewWillDisappear


In my iOS (Swift 3, Xcode 8) Core Data app I have 2 View Controllers (CategoriesViewController and ObjectsViewController) (Both are inside the same navigation controller).

Each ViewController has it's own tableView and it's own fetchResultsController to manage the results returned from Core Data request (Fetching entities titled Category in CategoriesViewController and entities titled Object in ObjectsViewController).

In my CategoriesViewController I have this variable:

var fetchResultsController: NSFetchedResultsController<Category>!

I've added the following code to CategoriesViewController to avoid having errors when opening another view :

override func viewWillDisappear(_ animated: Bool) {
          super.viewWillDisappear(true)

          self.fetchedResultsController.delegate = nil
          self.fetchedResultsController = nil

     }

In CategoriesViewController I've added these methods for fetchedResultsController :

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

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

 }

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

          switch type {
          case .update:
               guard let path = indexPath
                    else { return }
               tableView.reloadRows(at: [path], with: .automatic)

          case .delete:
               guard let path = indexPath
                    else { return }
               tableView.deleteRows(at: [path],
                                    with: .automatic)

          case .insert:
               guard let path = newIndexPath
                    else { return }
               tableView.insertRows(at: [path],
                                    with: .automatic)

          case .move:
               guard let _ = indexPath,
                    let _ = newIndexPath
                    else { return }
               // tableView.moveRow(at: fromPath, to: toPath)

               if indexPath != newIndexPath {
                    tableView.deleteRows(at: [indexPath!], with: .none)
                    tableView.insertRows(at: [newIndexPath!], with: .none)
               }



          }

     }

To fetch Core Data objects I wrote a coreData_fetchAll_Categories(). I've placed it into a viewWillAppear method of CategoriesViewController. After that i'm reloading data of a tableView.

 func coreData_fetchAll_Categories(handleCompleteFetching:@escaping (()->())) {

         let context = CoreDataManager.sharedInstance.viewContext

          let fetchRequest: NSFetchRequest<Category> = Category.fetchRequest()

          var sortDescriptors = [NSSortDescriptor]()

          let indexSortDescriptior = NSSortDescriptor(key: "indexOrder", ascending: true)
          sortDescriptors.append(indexSortDescriptior)

          fetchRequest.sortDescriptors = sortDescriptors

          self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context!, sectionNameKeyPath: nil, cacheName: nil)

          self.coreDataFetchedResultsController.delegate = self

          do {  try self.fetchedResultsController.performFetch()


          } catch  {
               print("performFetch() finished with error")
          }

     }

With the above code, after i'm returning back from my ObjectsViewController (where I also have all the methods with fetchedResultsController for Object entity and I also set fetchedResultsController to nil there in viewWillDisappear) my tableView in CategoriesViewController freezes. If I delete these 2 lines from viewWillDisappear of CategoriesViewController, everything works fine, but I need these lines to avoid another bugs.

self.fetchedResultsController.delegate = nil self.fetchedResultsController = nil

Code in ViewWillAppear looks like this:

 override func viewWillAppear(_ animated: Bool) {
          super.viewWillAppear(true)

         self.tableView.register(UINib.init(nibName: “CategoriesTableViewCell", bundle: nil), forCellReuseIdentifier: “categoriesTableViewCell")


          self.tableView.delegate = self
          self.tableView.dataSource = self


self.coreData_fetchAll_Categories {

  DispatchQueue.main.async { // Submit to the main Queue async


                    self.tableView.reloadData()

               }

}

}

After a CategoriesViewController appears a VC creates new version of fetchedResultsController (I've checked , it is not nil). Also i've noticed that tableView doesn't call cellForRowAt indexPath: . Seems Strange. delegates of tableView set to self in viewWillAppear.

I don't understand why can this bug happen, because i don't receive any errors.

Any opinions for this bug welcome. Working in Swift 3 (XCode 8).


Solution

  • You have to make sure that the tableview and the fetchedResultsController are never out of sync. Here are a few places that it can happen in your code:

    1. When you nil out the fetchedResultsController you must also reload the tableview, because now it should have zero section and zero rows
    2. coreData_fetchAll_Categoriesis already running on the main thread - there is no reason for a completion hander like that. Furthermore the use of DispatchQueue.main.async can cause real harm as there is a period of time before reloadData is called when the tableview and fetchedResultsController are out of sync
    3. Also (unrelated to your core data problems) when you call super.viewWillDisappear and super.viewWillAppear you should pass along the animated parameter - and not always pass true

    Hope that helps