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.
Related
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")
}
}
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
My problem is I get CoreData Serious error but I don't know why. The error is similar to:
Core Data Serious Application Error at controllerDidChangeContent
I have a controller displaying a list of Users (stored in User table in CoreData). Very straight forward.
In a second view controller, I retrieve a list of Users from a server and I also retrieve the Users stored in CoreData to see If we already have the same user locally (In which case I change the color background of the Cell) and I also add a header with a title in the first Cell of the first section.
Here is how I initialize my fetchController in the 1st viewController init constructor:
let modelMethodNameForSection = "firstCharOfFirstname"
let requestHelper = NSFetchRequest(entityName: modelEntityClass)
requestHelper.predicate = NSPredicate(format: "friend.groupA == YES")
requestHelper.sortDescriptors = [sortDescriptor]
let managedObjectCtx = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
fetchedResultsController = NSFetchedResultsController(fetchRequest: requestHelper, managedObjectContext: managedObjectCtx, sectionNameKeyPath: modelMethodNameForSection, cacheName: nil)
Here are the delegates:
//MARK: - NSFetchedResultsControllerDelegate
func controllerWillChangeContent(controller: NSFetchedResultsController)
{
tableView!.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController)
{
tableView!.endUpdates()
if let fetchedObjects = controller.fetchedObjects {
segmentedControlDelegate?.updateSegment(fetchedObjects.count, listType: ProfilePeopleListType.Friends) //listType doesn t matter here
}
}
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 {
if let cell = tableView!.cellForRowAtIndexPath(indexPath) as? UserViewCell { //We need to check that it's a UserViewCell, because it can be another class
configureCell(cell, atIndexPath: indexPath)
}
}
break
case .Move:
if let indexPath = indexPath {
tableView!.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
if let newIndexPath = newIndexPath {
tableView!.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
break
}
}
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:
return
}
}
Here is the code for the 2nd viewController displaying results merged with the server:
let modelMethodNameForSection = "firstCharOfFirstname"
let requestHelper = NSFetchRequest(entityName: modelEntityClass)
requestHelper.predicate = NSPredicate(format: "friend.groupA == YES")
requestHelper.sortDescriptors = [sortDescriptor]
let managedObjectCtx = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
fetchedResultsController = NSFetchedResultsController(fetchRequest: requestHelper, managedObjectContext: managedObjectCtx, sectionNameKeyPath: modelMethodNameForSection, cacheName: nil)
My tableview displays a header containing a title in rows[0] of sections[0] (top of the table)
//MARK: - NSFetchedResultsControllerDelegate
//When the local database Friends table changes we are notified
func controllerWillChangeContent(controller: NSFetchedResultsController)
{
}
func controllerDidChangeContent(controller: NSFetchedResultsController)
{
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
{
//We refresh the lists with new data from local Database
let listFriendsFilterBlock = peopleListWithOwnerFriendsFilter()
tableView?.reloadData()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType)
{
//We don't have any sections in the users list merged with server data
}
EDIT
Crash log:
[;2016-03-16 18:51:10.511 *** Assertion failure in -[UITableView
_endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3512.29.5/UITableView.m:1720
2016-03-16 18:51:10.511 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 (2) 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 (0 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)
From the error message your FRC is telling you (the delegate) that something has changed, as a result of this you're updating your data source data, but you aren't properly telling the table view about the insertions / deletions.
In this specific case 1 item has been inserted and the table view has not been told about any insertions.
This is possibly, though it's hard to tell, related to:
//We refresh the lists with new data from local Database
let listFriendsFilterBlock = peopleListWithOwnerFriendsFilter()
tableView?.reloadData()
because that kind of approach to refreshing the table should be implemented in controllerDidChangeContent not didChangeObject. You should know that the change is complete before reloading.
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?
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"