I've upgraded my app to Swift 2.0 and since then the NSFetchedResultsController doesn't behave correctly on iOS 8.4 (in iOS 9 it works as expected)
Scenario:
- A new entity is added
- Row appears in the tableview
- Property of the entity is changed so it shouldn't match the predicate of the fetchedtesultscontroller
- The row doesn't disappear from the tableview...
I can see the beginUpdates, the didChangeObject is called with the Delete type, and the endUpdates() is called.. My cache is nil.
Is this a known bug in xCode 7 with iOS 8.4? (the funny thing is that sometimes it DOES work, but most of the time it doesn't and it's crucial to my app...)
Thanks in advance!
p.s. I tried the if (indexPath != newIndexPath) stuff they say online, but the same result...
For anyone having the same issue, here is the solution:
In your:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
Make sure the Update case is BEFORE the Insert case... so instead of this:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade);
case .Move:
if(!indexPath!.isEqual(newIndexPath!)) {
self.tableView.moveRowAtIndexPath(indexPath!, toIndexPath: newIndexPath!);
}
case .Update:
let cell = self.tableView.cellForRowAtIndexPath(indexPath!) as! UITableViewCell;
self.configureCell(cell, atIndexPath: indexPath!);
self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade);
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
you should have this:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Update:
let cell = self.tableView.cellForRowAtIndexPath(indexPath!) as! UITableViewCell;
self.configureCell(cell, atIndexPath: indexPath!);
self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade);
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade);
case .Move:
if(!indexPath!.isEqual(newIndexPath!)) {
self.tableView.moveRowAtIndexPath(indexPath!, toIndexPath: newIndexPath!);
}
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
It gave me a headache but finally found the solution...
It looks like iOS 8 bug and you can find more info here: https://forums.developer.apple.com/thread/11662#65178
If you want to fix it, try this code:
public func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType(rawValue: 0)!:
break // iOS 8 bug - Do nothing if we get an invalid change type.
case .Insert:
// Insert here
break
case .Delete:
// Delete here
break
case .Move:
// Move here
break
case .Update:
// Update here
break
}
}
For my case, I had to do this instead:
case .Update:
to
case NSFetchedResultsChangeType(rawValue: 3)!:
For didChangeObject:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
print(type.rawValue)
switch type {
case .Insert:
print("didChangeObject.Insert")
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
print("didChangeObject.Delete")
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case NSFetchedResultsChangeType(rawValue: 3)!:
print("didChangeObject.Update")
self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, withObject: anObject as! NSManagedObject)
case .Move:
tableView.moveRowAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
default:
return
}
}
Related
I've problems with my custom sectionHeaderView. I'm getting the following error:
error: Serious application error. An exception was caught from the
delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:.
Invalid update: invalid number of sections. The number of sections contained in the table view after
the update (2) must be equal to the number of sections contained in
the table view before the update (2), plus or minus the number of
sections inserted or deleted (1 inserted, 0 deleted). with userInfo
(null)
If I use the normal titleForHeaderInSection, everything's working fine.
Changing to viewForHeaderInSection causes the error. That's the code for viewForHeaderinSection:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let currSection = fetchedResultsController.sections?[section] else { return nil }
let headerView = self.tableView.dequeueReusableHeaderFooterView(withIdentifier: ContractListHeaderView.reuseIdentifier) as! ContractListHeaderView
print(currSection.name)
headerView.sectionLabel.text = currSection.name
return headerView
}
And that's my fetchedResultsController:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
guard let newIndexPath = newIndexPath else { return }
tableView.insertRows(at: [newIndexPath], with: .automatic)
case .delete:
guard let indexPath = indexPath else { return }
tableView.deleteRows(at: [indexPath], with: .automatic)
case .update:
guard let indexPath = indexPath else { return }
tableView.reloadRows(at: [indexPath], with: .automatic)
case .move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return }
if indexPath == newIndexPath {
tableView.reloadData()
} else {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
}
}
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)
default:
break;
}
}
Thanks for the help
In my chat app, "lastMessage" is a relationship between the entities Friend and messages. Each Friend entity has a set of messages and only one lastMessage. Therefore, if i send a message to bill, bill's "lastMessage" will be updated to a newer Timestamp. That said, fetchedResultsController's fetched objects contains an array of each friend's lastMessage.timestamp . The problem I'm having is with the UITableView. If I message a friend a message in the chattingController and press back while simply having "messageTable.reloadData" inside of DidChangeContent, the friend I messaged, his lastMessage is updated to the latest timestamp and is sorted to be the first entry in the fetchedResultsController, moving him in the first spot in the tableview. However, as I wish to add animation by using self.messagesTable.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None) inside of didChangeObject, nothing is reloading properly. I tried putting it in .Insert, .Update, and .Move all at once. I'm fairly new to syncing NSFetchedResultsController to a tableView. So I'm not sure how reloading a table is normally achieved with NSFetchedResultsController.
#IBOutlet weak var messagesTable: UITableView!
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Friend")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastMessage.timestamp", ascending: false)]
let predicate = NSPredicate(format: "lastMessage.timestamp != nil")
fetchRequest.predicate = predicate
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
if type == .Insert {
self.messagesTable.reloadRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.None)//reloadItemsAtIndexPaths([indexPath!])
self.messagesTable.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)//reloadItemsAtIndexPaths([indexPath!])
}
if type == .Update{
// self.collectionView?.cha([newIndexPath!])
print("h")
self.messagesTable.reloadRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.None)//reloadItemsAtIndexPaths([indexPath!])
self.messagesTable.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)//reloadItemsAtIndexPaths([indexPath!])
}
if type == .Move{
// self.collectionView?.cha([newIndexPath!])
print("h")
self.messagesTable.reloadRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.None)//reloadItemsAtIndexPaths([indexPath!])
self.messagesTable.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)//reloadItemsAtIndexPaths([indexPath!])
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.messagesTable.beginUpdates()
}
Instead of reloadRowsAtIndexPaths, use the insertRowsAtIndexPaths and deleteRowsAtIndexPaths methods. Also, use beginUpdates in the controllerWillChangeContent method, and endUpdates in controllerDidChangeContent. For example:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.messagesTable.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
self.messagesTable.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
self.messagesTable.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
default:
return
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
self.messagesTable.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
self.messagesTable.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
self.messagesTable.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Move:
self.messagesTable.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
self.messagesTable.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.messagesTable.endUpdates()
}
This code works correctly, deletes in Core Data. Table view set DELETE in red to the right but does not delete the row in the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
let manageObject: NSManagedObject = frc.objectAtIndexPath(indexPath) as! NSManagedObject
moc.deleteObject(manageObject)
do {
try moc.save()
} catch {
print("Failed to save")
return
}
}
If I do stop the App and then I run it again, the table view do not sample row deleted and sample rows that they remain.
Delete the item explicitly by adding
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
after moc.deleteObject()
or add the NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
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:
self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
self.tableView.reloadData()
}
I have UITableViewController. It has data from array of files from Document Directory. I want to delete row this method self.navigationItem.leftBarButtonItem = self.editButtonItem()
but I make it, file is deleted but row is not disappeared. I tried several method but they didn't help me. I make it many once with data from CoreData. But I don't know how make it here. Please help me.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
tableView.beginUpdates()
let currentSong = listOfMP3Files[indexPath.row]
println(currentSong)
let directory = fileManager.URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first as! NSURL
let url = directory.URLByAppendingPathComponent(currentSong)
// println(url)
// println(indexPath)
fileManager.removeItemAtURL(url, error: nil)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(NSArray(object: indexPath) as [AnyObject], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
} else if editingStyle == .Insert {
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Update:
tableView.cellForRowAtIndexPath(indexPath!)
default: break
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Insert:
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Move:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Update:
break
default: break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
The second variant
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
tableView.beginUpdates()
let currentSong = listOfMP3Files[indexPath.row]
println(currentSong)
let directory = fileManager.URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first as! NSURL
let url = directory.URLByAppendingPathComponent(currentSong)
// println(url)
// println(indexPath)
fileManager.removeItemAtURL(url, error: nil)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
} else if editingStyle == .Insert {
}
}
You are missing a line to call removeAtIndex() which removes the item at specified index from datasource array
tableView.beginUpdates()
listOfMP3Files.removeAtIndex(indexPath.row) //Add this line
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.endUpdates()
In UITableViewController I can insert row and section at the same time with this implementations:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController!, didChangeObject anObject: AnyObject!, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
if controller == frc {
switch(type) {
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
case .Update:
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
default:
break
}
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch(type) {
case .Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
default:
break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
What about UICollectionViewController? I haven't found alternative way to implement controllerWillChangeContent and controllerDidChangeContent.
collectionView works somewhat different than tableView, you need to use performBatchUpdates
var frc: NSFetchedResultsController?
var iip = [NSIndexPath]()
var dip = [NSIndexPath]()
var ins: NSIndexSet?
var des: NSIndexSet?
func controllerWillChangeContent(controller: NSFetchedResultsController) {
}
func controller(controller: NSFetchedResultsController!, didChangeObject anObject: AnyObject!, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
if controller == frc {
switch(type) {
case .Insert:
iip.append(newIndexPath)
case .Delete:
dip.append(indexPath)
case .Update:
self.collectionView!.reloadItemsAtIndexPaths([indexPath])
default:
break
}
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch(type) {
case .Insert:
ins = NSIndexSet(index: sectionIndex)
case .Delete:
des = NSIndexSet(index: sectionIndex)
default:
break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.collectionView!.performBatchUpdates({
self.collectionView!.insertItemsAtIndexPaths(self.iip)
self.collectionView!.deleteItemsAtIndexPaths(self.dip)
if self.ins != nil {
self.collectionView!.insertSections(self.ins!)
}
if self.des != nil {
self.collectionView!.deleteSections(self.des!)
}
}, completion: {completed in
self.iip.removeAll(keepCapacity: false)
self.dip.removeAll(keepCapacity: false)
self.ins = nil
self.des = nil
})
}
I was able to do this by checking to see if the section of the new index path is greater than the number of sections in the collection view, and then performing batch updates.
let indexPath = IndexPath()
// index path of item to be inserted
if indexPath.section > collectionView.numberOfSections - 1 {
collectionView.performBatchUpdates({
let set = IndexSet(integer: sectionIndex)
collectionView.insertSections(set)
self.collectionView.insertItems(at: [indexPath])
}, completion: nil)
} else {
collectionView.insertItems(at: [indexPath])
}