nsfetchedResultsController - error when merging data - ios

I had some issue with NSFetchedResultsController.
I had subclass FRC
class InboxFetchResultsController: NSFetchedResultsController {
override init() {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let fetchRequest = NSFetchRequest(entityName: "Compliment")
let firstSortDescriptor = NSSortDescriptor(key: "updatedAt", ascending: false)
let secondSortDescriptor = NSSortDescriptor(key: "sendedDate", ascending: false)
fetchRequest.sortDescriptors = [firstSortDescriptor, secondSortDescriptor]
let privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateManagedObjectContext.parentContext = appDelegate.cdh.managedObjectContext
let user = try! privateManagedObjectContext.existingObjectWithID(CoreDataManager.sharedInstance.getLoggedUser().objectID) as! User
let predicate = NSPredicate(format: "recievedBelong = %#", user)
fetchRequest.predicate = predicate
super.init(fetchRequest: fetchRequest, managedObjectContext: privateManagedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
//
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(contextDidSaveContext(_:)), name: NSManagedObjectContextDidSaveNotification, object: nil)
}
deinit{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func contextDidSaveContext(notification: NSNotification) {
let sender = notification.object as! NSManagedObjectContext
if sender != managedObjectContext {
self.managedObjectContext.performBlock({ [weak self] in
DDLogInfo("Core data merging")
self!.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
})
}
}
}
And with ComplimentsViewController
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchedResultsController = InboxFetchResultsController()
fetchedResultsController.delegate = self
fetchedResultsController.fetchRequest.predicate = self.receivedPredicate
return fetchedResultsController
}()
In viewDidLoad I'm calling
func performFetch() {
fetchedResultsController.managedObjectContext.performBlock { [weak self] in
do {
try self!.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
dispatch_async(dispatch_get_main_queue(), {
self!.tableView.reloadData()
})
}
with
extension ComplimentsViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(controller: NSFetchedResultsController) {
dispatch_async(dispatch_get_main_queue(), {
self.tableView.beginUpdates()
})
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
dispatch_async(dispatch_get_main_queue(), {
self.tableView.endUpdates()
})
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch (type) {
case .Insert:
if let indexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
break;
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
break;
case .Update:
if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
break;
case .Move:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
break;
}
}
I'm editing fetched object in different View Controller, using ViewModel, that is allocated on background Context.
When I save that background context, I get that error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack:
I spend hours to take care about the concurrency,
hours to check the tableView delegate methods...
And still get that issue.

You're using NSFetchedResultsController so you shouldn't call :
dispatch_async(dispatch_get_main_queue(), {
self!.tableView.reloadData()
})
Since the all NSFetchedResultsControllerDelegate method are here to update your tableView, it will be triggered automatically.
Also you don't need semicolon in Swift, neither to break; in your switch the default behavior is to break unless you explicitly mention fallthrough

Related

didChangeSection not being called with CoreData relationships in swift

I've been building up my app slowly. Previously, I had two entities in CoreData. One called Items and the other called Catalog. The goal was to have my app remember items that were added so the user wouldn't have to add in all the attributes again, and Catalog would store all the attributes of the item but Items would be the current active instance of the item.
Below is my code I had working where both Catalog and Items were duplicating information, and if I updated a record, both didChangeObject and didChangeSection were called correctly and my tableview would update correctly.
func editItem() {
item!.name = trimSpaces(itemName.text!)
item!.brand = trimSpaces(itemBrand.text!)
item!.qty = Float(itemQty.text!)
item!.price = currencyToFloat(itemPrice.text!)
item!.size = trimSpaces(itemSize.text!)
item!.section = trimSpaces(itemSection.text!)
item!.image = UIImageJPEGRepresentation(itemImage.image!, 1)
catalog!.name = trimSpaces(itemName.text!)
catalog!.brand = trimSpaces(itemBrand.text!)
catalog!.qty = Float(itemQty.text!)
catalog!.price = currencyToFloat(itemPrice.text!)
catalog!.size = trimSpaces(itemSize.text!)
catalog!.section = trimSpaces(itemSection.text!)
catalog!.image = UIImageJPEGRepresentation(itemImage.image!, 1)
do {
try moc.save()
} catch {
fatalError("Edit Item save failed")
}
if (itemProtocal != nil) {
itemProtocal!.finishedEdittingItem(self, item: self.item!)
}
}
But when I remove all the duplicate attributes between Catalog and Items and add a one to one relationship. Now only didChangeObject gets called but not didChangeSection. * Updated with pbasdf suggestions *
func editItem() {
let item: Items = self.item!
item.qty = Float(itemQty.text!)
item.catalog!.name = trimSpaces(itemName.text!)
item.catalog!.brand = trimSpaces(itemBrand.text!)
item.catalog!.qty = Float(itemQty.text!)
item.catalog!.price = currencyToFloat(itemPrice.text!)
item.catalog!.size = trimSpaces(itemSize.text!)
item.catalog!.section = trimSpaces(itemSection.text!)
item.catalog!.image = UIImageJPEGRepresentation(itemImage.image!, 1)
do {
try moc.save()
} catch {
fatalError("Edit Item save failed")
}
if (itemProtocal != nil) {
itemProtocal!.finishedEdittingItem(self, item: self.item!)
}
}
**Note: in the first code I'm accessing the item and catalog variables that are set at the Class level. But in the new version, I wanted it to be unwrapped, so I am defining the item inside the function instead. The catalog is now coming from the relationship though.
Below are my didChangeObject and didChangeSection code, which doesn't change between the new and old version.
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Update:
self.tableView.reloadSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Move:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case NSFetchedResultsChangeType.Update:
self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
case NSFetchedResultsChangeType.Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
I'm still pretty new at programming in iOS, but my guess of what is happening is that the tableview is being populated by the Items Entity. Before, the tableview Section was being populated by item.section (which was inside Items) but now tableview Section is coming from the relationship with Catalog (items.catalog > catalog.section) if that makes any sense. So then when I update the managedObectContect, it isn't registering that the Section is being updated.
One thing to note though, when I close the app and open it again, it'll fix the Section. So it is loading correctly, and inserting correctly, just not updating correctly. I'm assuming that there is some other call I need to do to let the section know it is being changed, but I don't have a clue of what it would be, and google didn't know the answer, or more likely, with how I was asking it didn't know the answer.
Thank you for any help and let me know if you need me to include screenshots or additional code to explain better what is happening.
Adding in FRC and fetchRequest code
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Items")
let sortDesc1 = NSSortDescriptor(key: "catalog.section", ascending: true)
let sortDesc2 = NSSortDescriptor(key: "isChecked", ascending: true)
let sortDesc3 = NSSortDescriptor(key: "catalog.name", ascending: true)
fetchRequest.sortDescriptors = [sortDesc1, sortDesc2, sortDesc3]
return fetchRequest
}
func getFCR() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "catalog.section" , cacheName: nil)
return frc
}
Adding in Create New Item Code
func createNewItem() {
//Items Class Save
var entityDesc = NSEntityDescription.entityForName("Items", inManagedObjectContext: moc)
let item = Items(entity: entityDesc!, insertIntoManagedObjectContext: moc)
let id = NSUserDefaults.standardUserDefaults().integerForKey("nextItemID")
if (itemQty.text! == "") {
item.qty = 1
} else {
item.qty = Float(itemQty.text!)
}
item.isChecked = false
//Catalog Clase Save
entityDesc = NSEntityDescription.entityForName("Catalog", inManagedObjectContext: moc)
let catalog = Catalog(entity: entityDesc!, insertIntoManagedObjectContext: moc)
catalog.id = id
catalog.name = trimSpaces(itemName.text!)
catalog.brand = trimSpaces(itemBrand.text!)
if (itemQty.text == "") {
catalog.qty = 1
} else {
catalog.qty = Float(itemQty.text!)
}
catalog.price = currencyToFloat(itemPrice.text!)
catalog.size = trimSpaces(itemSize.text!)
catalog.section = trimSpaces(itemSection.text!)
catalog.image = UIImageJPEGRepresentation(itemImage.image!, 1)
item.catalog = catalog
do {
try moc.save()
NSUserDefaults.standardUserDefaults().setObject(id + 1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
} catch {
fatalError("New item save failed")
}
}

NSFetchedResultsController pushes updates without having pushed any inserts

I have a NSFetchedResultsController initialized like so:
lazy var postsResultsController: NSFetchedResultsController = {
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Post")
// Add Sort Descriptors
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
let predicate = NSPredicate(format: "owner == %#" , "owner",self.user)
fetchRequest.sortDescriptors = [sortDescriptor]
// Initialize Fetched Results Controller
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: StateControl.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
// Configure Fetched Results Controller
fetchedResultsController.delegate = self
return fetchedResultsController
}()
and I set my controller as the delegate. Here is an overview of the delegate methods:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
NSLog("Begin updates")
userPosts.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
NSLog("End updates")
userPosts.endUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch (type) {
case .Insert:
if let indexPath = newIndexPath {
userPosts.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
}
case .Delete:
if let indexPath = indexPath {
userPosts.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
}
case .Update:
print("Update request")
if let indexPath = indexPath {
userPosts.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
case .Move:
if let indexPath = indexPath {
userPosts.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
if let newIndexPath = newIndexPath {
userPosts.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
}
}
And below you see my UITableViewDataSource delegate methods:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return postsResultsController.sections?.count ?? 0
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = postsResultsController.sections {
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
else {return 0}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = postsResultsController.objectAtIndexPath(indexPath) as! Post
let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifiers.PostCollectionViewCell, forIndexPath: indexPath) as! PostCell
cell.setup(withPost: post, fromUser: user)
return cell
}
I set myself as the delegate in my tableView's didSet:
var userPosts: PostsTableView! {
didSet {
userPosts.delegate = self
userPosts.dataSource = self
userPosts.scrollEnabled = false
}
}
However, when I arrive to viewDidLoad:, I do the following:
do {
try self.postsResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
userPosts.reloadData()
And yet, my tableView remains completely empty. What I have noticed is that my log shows that userPostsController is pushing update requests but no inserts.
2016-02-29 15:31:52.073 Columize[10373:149421] Begin updates
Update request
2016-02-29 15:31:52.073 Columize[10373:149421] End updates
2016-02-29 15:31:52.074 Columize[10373:149421] Begin updates
Update request
2016-02-29 15:31:52.074 Columize[10373:149421] End updates
So, my best guess is that the NSFetchedResultsController just watches the context and passes on updates (I do a lot of fetching from backend that merge with existing objects, hence the many updates), but it was my understanding that every time I create a new NSFecthedResultsController it would populate and pass on the appropriate delegate calls (mainly inserts).
Furthermore, I'm assuming that means that after a NSManagedObjectContext has interacted with a NSFecthedResultsController, subsequent NSFecthedResultsControllers simply don't push Inserts.
1-Is my reasoning correct?
2-How can I circumvent that? Do I have to create a new context for every NSFecthedResultsController?
UPDATE
So the reason nothing was showing was because my tableView had height of zero (wooops). However nothing will appear if I don't call reloadData after the first fetch, so my question stands, namely, why aren't there any inserts performed by my NSFetchedResultsController?

CoreData with NSFetchResultController :Invalid update with with a property in fetchObjects

I have a serious problem reported by Xcode :
1. I have a NSFetchResultsController
var fetchedResultsController: NSFetchedResultsController {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest = NSFetchRequest()
// Edit the entity name as appropriate.
let entity = NSEntityDescription.entityForName("Choice", inManagedObjectContext: NSManagedObjectContext.MR_defaultContext())
fetchRequest.entity = entity
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 10
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "id", ascending: false)
let sortDescriptors = [sortDescriptor]
fetchRequest.sortDescriptors = sortDescriptors
//NSPredicate
let predicate = NSPredicate(format: "decision.id = %#", decision.id!)
fetchRequest.predicate = predicate
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.MR_defaultContext(), sectionNameKeyPath: nil, cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do{
try _fetchedResultsController?.performFetch()
}catch let error as NSError {
print(error)
}
return _fetchedResultsController!
}
2. And I have a button do add action:
#IBAction func didTouchAddChoiceButton(sender: UIButton) {
let choice = Choice.MR_createEntity() as! Choice
choice.id = GDUtils.CMUUID()
choice.decision = decision
NSManagedObjectContext.MR_defaultContext().MR_saveToPersistentStoreAndWait()
}
3. After adding this Entity. I have a controller to handle updating tableView like this
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch(type){
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Top)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
tableView.cellForRowAtIndexPath(indexPath!)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Left)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
}
4.But the problem happened : every time I try to change a property of an entity from fetchedObjects :
let chosenChoice = fetchedResultsController.objectAtIndexPath(currentIndextPath!) as! Choice
chosenChoice.name = tableCell.choiceName.text
I got this message :
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
Can anyone help me to figure out what happened ?
There's a bug in NSFetchedResultsController on iOS8, where FRC calls .Insert instead of .Update. I solved it the way, that I'm reloading the table completely, when .Insert is called on iOS8.
case .Insert:
guard let newIndexPath = newIndexPath else { return }
// iOS8 bug when FRC calls insert instead of Update
if #available(iOS 9, *) {
// insert item normally
} else {
// reload everything
}
Make sure that the managedOjectContext you assigned to NSFetchResultsController is same where you created new Choice NSManagedObject.
ADDED
in "case .Update" code gives you cell of tableview. and you need to update tableView cell data for not getting that error. atleast change/(pretend change) sth in cell.
cell = tableView.cellForRowAtIndexPath(indexPath!)
cell.choiceName.text = "empty"

Deleting from UISearchController's filtered search results

I have a tableView sourcing its cell content from CoreData and have been replacing the SearchDisplayController (deprecated) with the new SearchController. I am using the same tableView controller to present both the full list of objects and also the filtered/searched objects.
I have managed to get the search/filtering working fine and can move from the filtered list to detail views for those items, then edit and save changes back successfully to the filtered tableView. My problem is swiping to delete cells from the filtered list causes an run time error. Previously with the SearchDisplayController I could do this easily as I had access to the SearchDisplayController's results tableView and so the following (pseudo) code would work fine:
func controllerDidChangeContent(controller: NSFetchedResultsController) {
// If the search is active do this
searchDisplayController!.searchResultsTableView.endUpdates()
// else it isn't active so do this
tableView.endUpdates()
}
}
Unfortunately no such tableView is exposed for the UISearchController and Im at a loss. I have tried making the tableView.beginUpdates() and tableView.endUpdates() conditional on tableView not being the search tableView but with no success.
For the record this is my error message:
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-3318.65/UITableView.m:1582
* EDIT *
My tableView uses a FetchedResultsController to populate itself from CoreData. This tableViewController also the one used by the SearchController to display filtered results.
var searchController: UISearchController!
Then in ViewDidLoad
searchController = UISearchController(searchResultsController: nil)
searchController.dimsBackgroundDuringPresentation = false
searchController.searchResultsUpdater = self
searchController.searchBar.sizeToFit()
self.tableView.tableHeaderView = searchController?.searchBar
self.tableView.delegate = self
self.definesPresentationContext = true
and
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchText = self.searchController?.searchBar.text
if let searchText = searchText {
searchPredicate = searchText.isEmpty ? nil : NSPredicate(format: "locationName contains[c] %#", searchText)
self.tableView.reloadData()
}
}
So far as the error message is concerned, I'm not sure how much I can add. The app hangs immediately after pressing the red delete button (Which remains showing) revealed by swiping. This is the thread error log for 1 - 5. The app seems to hang on number 4.
#0 0x00000001042fab8a in objc_exception_throw ()
#1 0x000000010204b9da in +[NSException raise:format:arguments:] ()
#2 0x00000001027b14cf in -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] ()
#3 0x000000010311169a in -[UITableView _endCellAnimationsWithContext:] ()
#4 0x00000001019b16f3 in iLocations.LocationViewController.controllerDidChangeContent (iLocations.LocationViewController)(ObjectiveC.NSFetchedResultsController) -> () at /Users/neilmckay/Dropbox/Programming/My Projects/iLocations/iLocations/LocationViewController.swift:303
#5 0x00000001019b178a in #objc iLocations.LocationViewController.controllerDidChangeContent (iLocations.LocationViewController)(ObjectiveC.NSFetchedResultsController) -> () ()
I hope some of this helps.
* EDIT 2 *
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let location: Location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
location.removePhotoFile()
let context = self.fetchedResultsController.managedObjectContext
context.deleteObject(location)
var error: NSError? = nil
if !context.save(&error) {
abort()
}
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.searchPredicate == nil {
let sectionInfo = self.fetchedResultsController.sections![section] as NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
} else {
let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
}
return filteredObjects == nil ? 0 : filteredObjects!.count
}
}
// MARK: - NSFetchedResultsController methods
var fetchedResultsController: NSFetchedResultsController {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest = NSFetchRequest()
// Edit the entity name as appropriate.
let entity = NSEntityDescription.entityForName("Location", inManagedObjectContext: self.managedObjectContext!)
fetchRequest.entity = entity
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
if sectionNameKeyPathString1 != nil {
let sortDescriptor1 = NSSortDescriptor(key: sectionNameKeyPathString1!, ascending: true)
let sortDescriptor2 = NSSortDescriptor(key: sectionNameKeyPathString2!, ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
} else {
let sortDescriptor = NSSortDescriptor(key: "firstLetter", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
}
var sectionNameKeyPath: String
if sectionNameKeyPathString1 == nil {
sectionNameKeyPath = "firstLetter"
} else {
sectionNameKeyPath = sectionNameKeyPathString1!
}
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: sectionNameKeyPath, cacheName: nil /*"Locations"*/)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
var error: NSError? = nil
if !_fetchedResultsController!.performFetch(&error) {
fatalCoreDataError(error)
}
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController? = nil
func controllerWillChangeContent(controller: NSFetchedResultsController) {
if searchPredicate == nil {
tableView.beginUpdates()
} else {
(searchController.searchResultsUpdater as LocationViewController).tableView.beginUpdates()
}
// tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
var tableView = UITableView()
if searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
}
switch type {
case .Insert:
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
default:
return
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath) {
var tableView = UITableView()
if searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
}
switch type {
case .Insert:
println("*** NSFetchedResultsChangeInsert (object)")
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Delete:
println("*** NSFetchedResultsChangeDelete (object)")
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
case .Update:
println("*** NSFetchedResultsChangeUpdate (object)")
if searchPredicate == nil {
let cell = tableView.cellForRowAtIndexPath(indexPath) as LocationCell
let location = controller.objectAtIndexPath(indexPath) as Location
cell.configureForLocation(location)
} else {
let cell = tableView.cellForRowAtIndexPath(searchIndexPath) as LocationCell
let location = controller.objectAtIndexPath(searchIndexPath) as Location
cell.configureForLocation(location)
}
case .Move:
println("*** NSFetchedResultsChangeMove (object)")
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
if searchPredicate == nil {
tableView.endUpdates()
} else {
(searchController.searchResultsUpdater as LocationViewController).tableView.endUpdates()
}
}
The problem arises because of a mismatch between the indexPath used by the fetched results controller and the indexPath for the corresponding row in the tableView.
Whilst the search controller is active, the existing tableView is reused to display the search results. Hence your logic to differentiate the two tableViews:
if searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
}
is unnecessary. It works, because you set searchController.searchResultsUpdater = self when you initialise the searchController, so there is no need to change it, but the same tableView is used in either case.
The difference lies in the way the tableView is populated whilst the searchController is active. In that case, it looks (from the numberOfRowsInSection code) as though the filtered results are all displayed in one section. (I assume cellForRowAtIndexPath works similarly.) Suppose you delete the item at section 0, row 7, in the filtered results. Then commitEditingStyle will be called with indexPath 0-7, and the following line:
let location: Location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
will try to get the object at index 0-7 from the FRC. But the item at index 0-7 of the FRC might be a completely different object. Hence you delete the wrong object. Then the FRC delegate methods fire, and tell the tableView to delete the row at index 0-7. Now, if the object really deleted was NOT in the filtered results, then the count of rows will be unchanged, even though a row has been deleted: hence the error.
So, to fix it, amend your commitEditingStyle so that it finds the correct object to delete, if the searchController is active:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
var location : Location
if searchPredicate == nil {
location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
} else {
let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
}
location = filteredObjects![indexPath.row] as Location
}
location.removePhotoFile()
let context = self.fetchedResultsController.managedObjectContext
context.deleteObject(location)
var error: NSError? = nil
if !context.save(&error) {
abort()
}
}
}
I haven't been able to test the above; apologies if some errors slipped in. But it should at least point in the right direction; hope it helps. Note that similar changes may be required in some of the other tableView delegate/datasource methods.

NSFetchedResultController sorting sections by date gets rearranged

I have a problem with NSFetchedResultController. I have a button that saves two values to Core Data: actualDate and shortDate. The actualDate is an NSDate variable (eg. 2014-12-04 08:35:59 +0000) and the shortDate is a String, created from actualDate via NSDateFormatter (eg. 04 dec).
Below this button is a tableView that should display all entries, sorted by date in descending order with shortDate as the section name for each day(eg. 04 dec). This works fine with my current code WHILE RUNNING the app.
The problem appears when I close the app and start it again. The entries are still in the right order, but the section names is rearranged so that, for instance, 30 november is displayed before 4 december. I really don't know what the problem is or how to solve it.
Any help is appreciated!
Here's my code for NSFetchedResultController:
// MARK: - Fetched results controller
var fetchedResultsController: NSFetchedResultsController {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest = NSFetchRequest()
// Edit the entity name as appropriate.
let entity = NSEntityDescription.entityForName("List", inManagedObjectContext: self.managedObjectContext!)
fetchRequest.entity = entity
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "actualDate", ascending: false)
let sortDescriptor2 = NSSortDescriptor(key: "shortDate", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor, sortDescriptor2]
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: "shortDate", cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
var error: NSError? = nil
if !_fetchedResultsController!.performFetch(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
//println("Unresolved error \(error), \(error.userInfo)")
abort()
}
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController? = nil
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
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:
return
}
}
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)
default:
return
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
I know this is a old question but I faced the same problem and somehow I managed to solve this by setting a cacheName instead of letting it nil.
I answered this old question for everyone facing the same problem.

Resources