I have a UITableView backed by an NSFetchedResultsController.
The table view presents chat conversations.
I have a few chat conversations type (NSNumber), the table view separated for sections depending on your chat conversation type, So, if you have a 3 conversations type you have 3 sections in your table view.
The chat conversations sorted by date (each conversation contains last message dade).
The problem is when the user update the chat conversation type, the app crash with the following error:
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. *** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0] with userInfo (null)
2015-12-14 18:18:24.831 Reporty[1328:108995] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
I Initializes the NSFetchedResultsController with the following code:
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "ChatConversation")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "conversationType", ascending: false),
NSSortDescriptor(key: "lastMessageDate", ascending: false)
]
let frc = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: self.chatManager.getChatMainContext(),
sectionNameKeyPath: "conversationType",
cacheName: nil)
frc.delegate = self
return frc
}()
My NSFetchedResultsController delegate code:
extension ChatConversationsViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controller(
controller: NSFetchedResultsController,
didChangeSection sectionInfo: NSFetchedResultsSectionInfo,
atIndex sectionIndex: Int,
forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
default:
break
}
}
func controller(
controller: NSFetchedResultsController,
didChangeObject anObject: AnyObject,
atIndexPath indexPath: NSIndexPath?,
forChangeType type: NSFetchedResultsChangeType,
newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
configureCell(tableView.cellForRowAtIndexPath(indexPath!) as! ChatConversationTableViewCell, atIndexPath: indexPath!)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
}
All the core data model update with the same NSManagedObjectContext.
I'm using with https://github.com/jessesquires/JSQCoreDataKit wrapper
self.stack!.mainContext.performBlockAndWait {
//update the model
saveContext(self.stack!.mainContext) { (result: CoreDataSaveResult) in
switch result {
case .Success():
print("Success")
case .Failure(let error):
print("Error: \(error)")
}
}
}
Any advice?
Thanks
The NSFetchedResultsControllerDelegate methods are called inside an NSManagedObjectContextObjectsDidChangeNotification handler. You should put breakpoints in your method implementations for these in order to find the offending code. Chances are good it has to do with manipulating your table view and numberOfSectionsInTableView: or tableView:numberOfRowsInSection: not returning an appropriate value (so breakpoints in these methods may be useful as well).
Related
I am creating a todo list with CollectionView(VC1) and FRC.
I am first displaying VC1 on my homepage(in a section of another CVC) with a limit of items set to 3(Like a preview). Then I have a button which presents VC1 fullscreen and displays the entire count of items.
So far fetch limit is working correctly. However when I am on VC1 fullscreen I can update/delete items, now when I perform collectionView.performBatchUpdates I delete/update items and both instances of the views and updated accordingly. But...
If I add 3 more items, VC1 on the home page: limit is not 3 anymore but 6.
If I delete 2 items, VC1 on the home page: is now only displaying 1 item even if in coreData I have another 20.
It seems that no matter what the limit is set to, how many ever I delete or insert the collectionView updates accordingly. Now should I recall fetch items every time?
Here is some code of how I set the limits and update collectionView
lazy var fetchedResultsController: NSFetchedResultsController<TodoItem> = {
let context = CoreDataManager.shared.persistentContainer.viewContext
let request: NSFetchRequest<TodoItem> = TodoItem.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(key: "date", ascending: false)
]
request.fetchLimit = mode == .fullscreen ? .max : 3
let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
do {
try frc.performFetch()
} catch let err {
print(err)
}
return frc
}()
// Number of items in CV
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let allItems = fetchedResultsController.sections?[section].numberOfObjects ?? 0
let numberOfRows = mode == .fullscreen ? allItems : min(allItems, 3)
return numberOfRows
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
itemChanges.append((type, indexPath, newIndexPath))
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView?.performBatchUpdates({
for change in self.itemChanges {
switch change.type {
case .insert:
self.collectionView?.insertItems(at: [change.newIndexPath!])
case .delete:
self.collectionView?.deleteItems(at: [change.indexPath!])
case .update:
self.collectionView?.reloadItems(at: [change.indexPath!])
case .move:
self.collectionView?.deleteItems(at: [change.indexPath!])
self.collectionView?.insertItems(at: [change.newIndexPath!])
#unknown default:
return
}
}
}, completion: { finished in
self.itemChanges.removeAll()
})
}
Thank you for any help in advance.
The closure of the lazy instantiated variable is executed only once.
You have to change the fetch request with appropriate fetch limit somewhere else.
I am trying two update elements in collection view. When I pass just one message it works nicely
func simulate() {
let delegate = UIApplication.shared.delegate as! AppDelegate
let context = delegate.persistentContainer.viewContext
FriendsController.createMessageWithText(text: "Here's a text message that was sent a few minutes ago...", friend: friend!, minutesAgo: 1, context: context)
//second message
FriendsController.createMessageWithText(text: "Another message to give you hard time.", friend: friend!, minutesAgo: 1, context: context)
do {
try context.save()
}
catch let error as NSError {
print("Error \(error.localizedDescription)")
}
}
but when I do same steps except passing two messages it throws error:
CoreData: error: Serious application error. Exception was caught
during Core Data change processing. This is usually a bug within an
observer of NSManagedObjectContextObjectsDidChangeNotification.
Invalid update: invalid number of items in section 0. The number of
items contained in an existing section after the update (8) must be
equal to the number of items contained in that section before the
update (6), plus or minus the number of items inserted or deleted from
that section (1 inserted, 0 deleted) and plus or minus the number of
items moved into or out of that section (0 moved in, 0 moved out).
with userInfo (null)
Maybe someone have any clue?
There are: NSFetchController and delegate function I implemented:
lazy var fetchedResultsController: NSFetchedResultsController = { () -> NSFetchedResultsController<Message> in
let fetchRequest = NSFetchRequest<Message>(entityName: "Message")
fetchRequest.sortDescriptors = [NSSortDescriptor(key:"date",ascending: true)]
fetchRequest.predicate = NSPredicate(format: "friend.name = %#", (self.friend?.name!)!)
let delegate = UIApplication.shared.delegate as! AppDelegate
let context = delegate.persistentContainer.viewContext
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .insert {
collectionView?.insertItems(at: [newIndexPath!])
collectionView?.scrollToItem(at: newIndexPath!, at: .bottom, animated: true)
}
}
If you want to insert(or delete, reload) more than one cell, than you need to make it in performBatchUpdates method of UICollectionView instance. You need to call it in func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>).
Look at this example: https://github.com/NoFearJoe/Timmee/blob/master/Timmee/Timmee/Sources/Core%20layer/Database/StorageObservable.swift
or:
https://gist.github.com/tempire/debbabb2cccafa90320e
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:
When you nil out the fetchedResultsController you must also reload the tableview, because now it should have zero section and zero rows
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
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
I am using NSFetchedResultsController to track and load UITableView with two sections(based on available true or false) using data from coredata entity. Coredata entity is populated by azure syncTable pull method. I call fetchedDataController?.performFetch() from ViewDidLoad() method in VC.
Data is divided into two sections - section 1 available is false, section2- avaliable is true in the entity data.
Here is code snippet of NSFRC initialization:
lazy var fetchedDataController: NSFetchedResultsController<Product>? = {
let req = NSFetchRequest<NSFetchRequestResult>(entityName: "Product")
req.sortDescriptors = [NSSortDescriptor(key: "available", ascending: true), NSSortDescriptor(key: "createdAt", ascending: false)]
guard let dbContext = self.appDelegate?.managedObjectContext else { return nil }
let controller = NSFetchedResultsController(fetchRequest: req, managedObjectContext: dbContext, sectionNameKeyPath: "available", cacheName: nil) as? NSFetchedResultsController<Product>
controller?.delegate = self
return controller
}()
Issues:
1. Even there is update in entity row data FRC delegate methods are not fired automatically. Like I changed available from true to false in the backend and did a SyncTable.pull, I can see that coredata table is updated with latest data( I opened the .sql file and able to see the data)
To overcome workaround I called perform fetch every time I do a pull, in this case when I scroll down to section 2 app is crashing with index error, that means it is not updating the table section and rows properly.
Delegate methods implementation:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let sectionIndexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert:
self.tableView.insertSections(sectionIndexSet, with: .fade)
case .update:
self.tableView.reloadSections(sectionIndexSet, with: .fade)
case .delete:
self.tableView.deleteSections(sectionIndexSet, with: .fade)
default: break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let newIndex = newIndexPath {
self.tableView.insertRows(at: [newIndex], with: .fade)
}
case .update:
if let index = indexPath {
self.tableView.reloadRows(at: [index], with: .fade)
}
case .move:
if let index = indexPath {
self.tableView.deleteRows(at: [index], with: .fade)
if let newIndex = newIndexPath {
self.tableView.insertRows(at: [newIndex], with: .fade)
}
}
case .delete:
if let index = indexPath {
self.tableView.deleteRows(at: [index], with: .fade)
}
default: break
}
}
I am stuck into this any help is appreciated.
Adding the observer for managedobjectcontext save solves this issue:
NotificationCenter.default.addObserver(self, selector: #selector(refreshContent), name: NSNotification.Name.NSManagedObjectContextDidSave, object: nil)
func refreshContent(notif: Notification)
{
self. fetchedDataController?.managedObjectContext.mergeChanges(fromContextDidSave: notifn)
}
A fetchedResultsController monitors a particular context - not the sql file. If you have more than one contexts in the app, then changes from one may not be merged into the other. So even if the sql files is updated the context will not be. When you create the context make sure to set the viewContext's automaticallyMergesChangesFromParent = true.
I have a NSFetchedResultsController that managed my UITableView data source.
I am trying to modify a property of a NSManagedObject called amountToCompute using a NSBatchUpdateRequest. So I create the batch update:
let batchUpdate = NSBatchUpdateRequest(entityName: "MyEntity")
batchUpdate.propertiesToUpdate = ["amountToCompute" : newAmount]
batchUpdate.resultType = .UpdatedObjectIDsResultType
I execute it:
var batchError: NSError?
let batchResult = managedContext.executeRequest(batchUpdate, error: &batchError) as! NSBatchUpdateResult?
And to update my current managed context, I update each managedObject in the managedContext and perform a new fetch of fetchedResultsController:
if let result = batchResult {
let objectIDs = result.result as! [NSManagedObjectID]
for objectID in objectIDs {
let managedObject: NSManagedObject = managedContext.objectWithID(objectID)
if !managedObject.fault {
managedContext.refreshObject(managedObject, mergeChanges: true)
}
if !fetchedResultsController.performFetch(&error) {
println("error: + \(error?.localizedDescription), \(error!.userInfo)")
}
}
}
I implemented some methods of the delegate NSFetchedResultsControllerDelegate to manage changes in the results sent by the NSFetchedResultsController:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
...
case .Update:
reloadRowsAtIndexPaths([indexPath!], animation: reloadRowsWithAnimation)
let myManagedObject = anObject as! MyManagedObject
println("update : \(myManagedObject.amountToCompute)")
...
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
I run the app on my 8.4 iOS simulator and everything goes fine.
println("update : \(myManagedObject.amountToCompute)") prints the new value.
I run the app on my iPhone 6 8.4.1 and the value is not updated, println("update : \(myManagedObject.amountToCompute)") prints the old value. The new value is saved properly but the changes don't appear in my table view while they do on the simulator.
What's wrong? How come it can be different whether I'm on the simulator or on my device. The versions are not exactly the same but I doubt Apple touched Core Data architecture in their last update.
This is a an unusual, but known problem that I ran into myself and spent a couple of days pulling out my hair until found this blog post, which worked for me.
https://stevenpsmith.wordpress.com/2011/08/12/nsfetchedresultscontroller-and-core-data-managed-object-updates/
It boiled down to adding this single line of code that set the staleness interval to zero:
[context setStalenessInterval:0];
If I'm reading your post right you're having the same issue. :)