Need your help guys !
I'm going mad on a bug on iOS 9 only.
As you can see below, it is going well on iOS 11
At the beginning, i've got 4 cells with a status "Nouveau" for new.
When I click on a "new" cell, the status change to "En attente" (for "waiting").
On iOS 9, everything is fine until the last cell. When I tap on the last "new" cell, it doesn't move to "waiting" and the "new" section is still there. After this, the table view is not getting updates anymore, it is going back to normal when i change to another viewController and go back to the one containing the table view.
I'm using NSFetchedResultsController. Data is good, good number of sections, good number of elements by section, good title.
At the start 1 sections with title "Nouveau" with 4 objects
At the end, 1 sections with title "En attente" with 4 objects
extension CRDashboardOrdersViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
print( self.fetchResult?.sections?.count ?? 0 )
return self.fetchResult?.sections?.count ?? 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print( self.fetchResult?.sections?.get(index: section)?.numberOfObjects ?? 0 )
return self.fetchResult?.sections?.get(index: section)?.numberOfObjects ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: CRDashboardOrderTableViewCell = tableView.dequeueReusableCell(withIdentifier: "CRDashboardOrderTableViewCell") as! CRDashboardOrderTableViewCell
let order: Order = self.fetchResult.object(at: indexPath) as! Order
cell.order = order
if let selectedIndexPath = self.selectedIndexPath, selectedIndexPath == indexPath {
self.tableView.selectRow(at: selectedIndexPath, animated: false, scrollPosition: .none)
}
return cell
}
}
extension CRDashboardOrdersViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.selectedIndexPath = indexPath
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
print( self.fetchResult?.sections?.get(index: section)?.name ?? "???" )
return self.fetchResult?.sections?.get(index: section)?.name ?? "???"
}
}
extension CRDashboardOrdersViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
print(sectionIndex)
print(sectionInfo.indexTitle)
switch type {
case .insert:
self.tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
case .delete:
self.tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
default:
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
self.tableView.reloadRows(at: [indexPath!], with: .fade)
if indexPath == self.selectedIndexPath {
self.selectedIndexPath = indexPath
}
break
case .insert:
self.tableView.insertRows(at: [newIndexPath!], with: .fade)
self.selectedIndexPath = nil
break
case .delete:
self.tableView.deleteRows(at: [indexPath!], with: .fade)
self.selectedIndexPath = nil
break
case .move:
self.tableView.moveRow(at: indexPath!, to: newIndexPath!)
if indexPath == self.selectedIndexPath {
self.selectedIndexPath = newIndexPath
}
break
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.endUpdates()
}
}
You have made a mistake with your cellForRow: and your FetchedResults delegate.
When using a FetchedResultsController, on .update you should not use reloadRows, this causes problems, instead you should have a method which updates a cells model like so:
func updateCell(atPath indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? MyCell {
updateCell(withCell: cell, atIndexPath: indexPath)
}
}
func updateCell(withCell cell: MyCell, atIndexPath indexPath: IndexPath) {
if let MyModel = fetchedResultsController?.object(at: indexPath) {
cell.model = myModel
}
}
Then in your delegate .update you call updateCell(AtIndexPath), to update the cells UI.
You should also use this cell configure method in your cellForRow
Related
I have 2 ViewControllers:
VC1 populates its tableView based on CoreData attribute isPicked, which is bool and show only items with true state. VC2 is a second Modal (not Full Screen) View Controller which allow user to change the state of isPicked attribute: check and uncheck item (make it true or false). The idea is user picks needed items end dismiss the VC2 and the items should appear in VC1.
I have implemented NSFetchedResultsController to both VC1 and VC2. And as soon as I press on first item (i.e. change isPicked state to true) I receive the error from VC1:
[error] fault: 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).
Here is how I change a state of item in VC2 (true\false):
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let currencyArray = fetchedResultsController.fetchedObjects else { return }
let cell = tableView.cellForRow(at: indexPath) as! PickCurrencyTableViewCell
for currency in currencyArray {
if currency.shortName == cell.shortName.text {
currency.isPicked = !currency.isPicked
coreDataManager.save()
}
}
}
Here is my VC1 NSFetchedResultsController implementation:
override func viewDidLoad() {
super.viewDidLoad()
setupFetchedResultsController()
}
func setupFetchedResultsController() {
let predicate = NSPredicate(format: "isForConverter == YES")
fetchedResultsController = createCurrencyFetchedResultsController(and: predicate)
fetchedResultsController.delegate = self
try? fetchedResultsController.performFetch()
}
func createCurrencyFetchedResultsController(with request: NSFetchRequest<Currency> = Currency.fetchRequest(), and predicate: NSPredicate? = nil, sortDescriptor: [NSSortDescriptor] = [NSSortDescriptor(key: "shortName", ascending: true)]) -> NSFetchedResultsController<Currency> {
request.predicate = predicate
request.sortDescriptors = sortDescriptor
return NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
}
Here is my VC1 NSFetchedResultsController delegate:
extension VC1: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if let indexPath = indexPath, let newIndexPath = newIndexPath {
switch type {
case .update:
tableView.reloadRows(at: [indexPath], with: .none)
case .move:
tableView.moveRow(at: indexPath, to: newIndexPath)
case .delete:
tableView.deleteRows(at: [indexPath], with: .none)
case .insert:
tableView.insertRows(at: [indexPath], with: .none)
default:
tableView.reloadData()
}
}
}
}
When I reload the app, picked item shows itself in VC1 (which caused crash). But every change in VC2 crashes the app again with the same error. I don't have any methods to delete items in VC1 or so. I need that VC1 tableView just show or hide items according to isPicked state made from VC2.
What I missed in my code?
UPDATE: my VC1 TableView methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let currency = fetchedResultsController.sections![section]
return currency.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell", for: indexPath) as! ConverterTableViewCell
let currency = fetchedResultsController.object(at: indexPath)
cell.shortName.text = currency.shortName
cell.fullName.text = currency.fullName
return cell
}
Doc states:
controller(_:didChange:at:for:newIndexPath:)
indexPath
The index path of the changed object (this value is nil for
insertions).
So, you should change the code like this:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
tableView.reloadData() // << May be needed ?
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
tableView.reloadData() // << May be needed ?
}
default:
tableView.reloadData()
}
}
}
I'm getting the following error when I delete a cell from the table view:
Invalid update: invalid number of sections. The number of sections
contained in the table view after the update (0) must be equal to the
number of sections contained in the table view before the update (1),
plus or minus the number of sections inserted or deleted (0 inserted,
0 deleted)
I'm using the NSFetchedResultsControllerDelegate to respond to the changes in the number of section. I'm retrieving the data from a plist file in Core Data and each addition of the table view cell increases the number of sections accordingly, so 3 cells = 3 sections.
override func numberOfSections(in tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let goal = fetchedResultsController.object(at: indexPath)
cell.textLabel!.text = goal.title
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let goal = fetchedResultsController.object(at: indexPath)
self.context.delete(goal)
self.saveContext()
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
default:
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let section = IndexSet(integer: sectionIndex)
switch type {
case .delete:
tableView.deleteSections(section, with: .automatic)
default:
break
}
}
context is this:
var context : NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
How I load the saved Core Data onto FRC:
func loadSavedData() {
if fetchedResultsController == nil {
let request = Goal.createFetchRequest()
let sort = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sort]
request.fetchBatchSize = 20
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: "title", cacheName: nil)
fetchedResultsController.delegate = self
}
fetchedResultsController.fetchRequest.predicate = goalPredicate
do {
try fetchedResultsController.performFetch()
tableView.reloadData()
} catch {
print("Fetch failed")
}
}
try calling beginUpdates and endUpdates in these delegate apis also.
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates
}
if you still face issues then in this delegate function, try to call like this:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.performBatchUpdates {
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.deleteSections(section, with: .automatic)
} completion: { (status) in
}
}
I have a pretty straight forward UITableViewController /NSFetchedResultsController case here. It's from Xcode Master-Detail App sample code, So easy to reproduce.
I have a CoreData Model with 1 Entity with 1 String Attribute. It's displayed in a UITableViewController. I use .subtitle system cell type.
By selecting a row, I simply update the String Attribute.
So my problem is, when I add just enough rows for the tableview to scroll (10-11 rows on a iPhone 5s with navbar), and I scroll down, and select any row, the tableview bounces up and down.
If there is less rows (less than 10 rows), or more rows (12 rows and more), the behaviour is normal.
So It seems to be at the limit of the scroll view the problem happens.
If I don't use beginUpdates()/endUpdates(), the problem goes away, but I loose their advantages.
Here is a video link of what happens
class TableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var managedObjectContext: NSManagedObjectContext? = nil
#objc func insertNewObject(_ sender: Any) {
let context = self.fetchedResultsController.managedObjectContext
let newEvent = Event(context: context)
newEvent.aString = "a String"
try? context.save()
}
override func viewDidLoad() {
super.viewDidLoad()
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
}
override func numberOfSections(in tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let event = fetchedResultsController.object(at: indexPath)
configureCell(cell, withEvent: event)
return cell
}
func configureCell(_ cell: UITableViewCell, withEvent event: Event) {
cell.textLabel?.text = event.aString
cell.detailTextLabel?.text = event.aString
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let event: Event = self.fetchedResultsController.object(at: indexPath)
event.aString = event.aString! + ""
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let context = fetchedResultsController.managedObjectContext
context.delete(fetchedResultsController.object(at: indexPath))
try? context.save()
}
}
var fetchedResultsController: NSFetchedResultsController<Event> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest: NSFetchRequest<Event> = Event.fetchRequest()
fetchRequest.fetchBatchSize = 20
let sortDescriptor = NSSortDescriptor(keyPath: \Event.aString, ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
try? _fetchedResultsController!.performFetch()
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController<Event>? = nil
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
switch type {
case .insert:
tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic)
case .delete:
tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic)
default:
return
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update:
configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! Event)
case .move:
configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! Event)
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
I was running into the same problem, and I found that implementing the estimatedHeightFor... methods fixed the issue for me. The issue seems to stem from using automatic cell heights in the table instead of explicitly defined heights per row.
The table I'm using has both rows and section header/footers, so I needed to define estimated heights for both rows and headers, and this solved the strange bouncing animation during batch updates.
Note that returning 0 for section header heights will use the table view's default value which is likely UITableViewAutomaticDimension, so return a positive number for the estimated heights.
My code in Obj-C:
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 44; // anything except 0 or UITableViewAutomaticDimension
}
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section{
return 18; // anything except 0 or UITableViewAutomaticDimension
}
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForFooterInSection:(NSInteger)section{
return 18; // anything except 0 or UITableViewAutomaticDimension
}
I am using UITableView with estimatedRowHeight and UITableViewAutomaticDimension. Also I am using NSFetchedResultsControllerDelegate to reflect the changes.
Everything is work fine. Now the problem is when ever I add some record to CoreData, NSFetchedResultsController called the it's delegate but an unexpected things happen. TableView suddenly scroll to Top every time.
NSFetchedResultsControllerDelegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .None)
break
case .Update:
tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
break
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
break
default: break;
}
}
By googling I found few answers where people suggested to use tableView: heightForRowAtIndexPath: but as my cell height is dynamic. So what should I do?
This is default behaviour of tableview, inserting an row in tableview scroll it to top.
I did faced the same issue with my app.
Here is how i able to manage the content offset of tableview, follow the steps,
1)Store the content offset of UITableview before inserting row or section.
2)Insert objects in array.
3)Reload the tableview
4)Subtract the difference and set the content offset manually.
let oldOffset: CGFloat = tblTest.contentSize.height
tblTest.reloadData()
let newOffset: CGFloat = tblTest.contentSize.height
tblTest.setContentOffset(CGPointMake(0, (newOffset - oldOffset)), animated: false)
This is how i have done in my applications so far, and with dynamic cell to overcome the reinitialise cell everytime, it is working just awesome.
Adding to #jayesh-miruliya 's answer, in your NSFetchedResultsControllerDelegate after the switch statement, put this in your code:
tableView.reloadData()
dispatch_async(dispatch_get_main_queue(), {
let topCellIndexPath = NSIndexPath(forRow: 0, inSection: 0)
self. tableView.scrollToRowAtIndexPath(topCellIndexPath, atScrollPosition: .Top, animated: true)
})
To solve my problem I save the cell height in the willDisplay method and in estimatedHeightForRowAt I'm retrieving the value.
var cellHeights = NSMutableDictionary()
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cellHeights.object(forKey: indexPath) {
return height as! CGFloat
}
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights.setObject(cell.frame.size.height, forKey: indexPath as NSCopying)
}
tableView.reloadData()
if tableView.numberOfRowsInSection(0) > 0
{
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self. tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Top, animated: true)
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .None)
break
case .Update:
tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
break
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
break
default: break;
}
tableView.reloadData()
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self. tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Top, animated: true)
}
}
let cou : Int = self.Array.count as Int
let lastindex : NSIndexPath = NSIndexPath(forRow: cou-1, inSection: 0)
self.TableName.scrollToRowAtIndexPath(lastindex, atScrollPosition: UITableViewScrollPosition.None, animated: true)
This code help you to scroll your tableview at particular position.
func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
tabelView.reloadData()
}
this is work for me
I'd like to delete a UITableViewCell & a CoreData Object from a UITableView with an animation. I wrote this code for doing it:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let mySelectedCell:UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
//1
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
//2
let fetchRequest = NSFetchRequest(entityName:"Person")
//3
mySelectedCell.backgroundColor = green
mySelectedCell.detailTextLabel?.text = "Done"
mySelectedCell.accessoryType = UITableViewCellAccessoryType.Checkmark
let person = people[indexPath.row]
managedContext.deleteObject(person)
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
}
I don't know why but the app is crashing if I select a cell, which I want to delete.
2015-03-29 18:00:10.776 MyApp[3001:79507] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-3318.93/UITableView.m:1582
Crashing line: self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
Does someone knows what I've done wrong?
Update:
tableView.beginUpdates()
var numberItems:Int = people.count
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
numberItems -= [indexPath].count // something like [indexPath].count
tableView.endUpdates()
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
application.applicationIconBadgeNumber = people.count
return people.count
}
New error:
2015-03-29 19:14:15.972 MyApp[3389:89566] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-3318.93/UITableView.m:1582
in this line of code: tableView.endUpdates()
If the table view displays Core Data objects using a fetched results controller then the only thing you have
to do to delete an object is to delete the object from the managed object context:
let context = self.fetchedResultsController.managedObjectContext
context.deleteObject(self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject)
and of course save the context to make the change permanent.
Updating the table view is then done "automatically" by the
fetched results controller delegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController)
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType)
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
func controllerDidChangeContent(controller: NSFetchedResultsController)
You'll find a typical implementation of these methods in the
NSFetchedResultsControllerDelegate documentation, if you haven't
implemented them already.
Also you should not have a "copy" of the fetched objects in your
people array. The fetched results controller is directly used
in the table view data source methods, for example:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.fetchedResultsController.sections?.count ?? 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = self.fetchedResultsController.sections![section] as NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
If you create a fresh "Master-Detail + Core Data Application" in Xcode
then you'll get all the necessary template code that you need.
You have to put yout method deleteRowsAtIndexPaths inside this two methods in the following way :
var path = [NSIndexPath]()
// This is only a example with the index [0..numberOfItemsToDelete-1]
for (var i = 0; i < numberOfItemsToDelete; i++) {
path.append(NSIndexPath(forRow: i, inSection: 0))
}
tableView.beginUpdates()
self.tableView.deleteRowsAtIndexPaths(path, withRowAnimation: .Right)
self.numberOfItems -= theNumberYouDeletedAbove // something like [indexPath].count
tableView.endUpdates()
And of course you have to update the numberOfRowsInSection method because it will throw a run-time error , you can do something like this :
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfItems
}
Where numberOfItems match the size of the array you use to display the items.
I hope this help you.
FIRST, remove this line which try to duplicate and involve managedContext job an cause crash.
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
SECOND, Now you should able to delete without crash and without animation.
THIRD, Let the NSFetchedResultsController delete and trigger animation in its way with "didChange" handler.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?)
{
print(indexPath)
if type == .delete{
tableView.deleteRows(at: [indexPath!], with: .automatic)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
//0.4 sec delay before refreshing the table
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 0.4) {
self.refresh()
}
}
func refresh(){
tableView.reloadData()
}
If you can not delete at SECOND step;
Check, if you've implemented:
NSFetchedResultsControllerDelegate
in the class definition and set the delegate in the class also e.g.
yourResultsController.delegate = self