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).
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:
coreData_fetchAll_Categories
is 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 syncsuper.viewWillDisappear
and super.viewWillAppear
you should pass along the animated
parameter - and not always pass true
Hope that helps