I am attempting to move rows containing core data objects in my table view. I get invalid update error.
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 (2), 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).'
I don't understand why I get this. I'm deleting the row I want to move at the source index path, then inserting it at the destination index path.
Here is my code:
// Move fetchedResultsController objects
override func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let movedObject = self.fetchedResultsController.objectAtIndexPath(sourceIndexPath) as! NSManagedObject
controller(self.fetchedResultsController, didChangeObject: movedObject, atIndexPath: sourceIndexPath, forChangeType: .Delete, newIndexPath: sourceIndexPath)
controller(self.fetchedResultsController, didChangeObject: movedObject, atIndexPath: sourceIndexPath, forChangeType: .Insert, newIndexPath: destinationIndexPath)
}
A fetchedResultsController job is to monitor core data changes and tell your UI that it needs to update to match what is now in core data. You don't call controller( didChangeObject:.. methods; the fetchedResultsController calls them.
You should just update the object in core-data wait for the fetchedResultsController to inform your viewController to update.
A move it is more complicated. The tableView has already been updated and now you want to update core-data. But when you update core-data the fetchedResultsController will kick in an tell you to do the move - which you already did! So instead set fetchedResultsController to nil, then update core-data (this would be the field that the objects are stored by in your fetch, maybe an order property or something like that), and then reset the fetchedResultsController's delegate.
The code below is swift 4:
In the situation where you have a table with multiple sections your moveRowAt fromIndexPath: IndexPath, to toIndexPath: IndexPath function needs to calculate the correct row to move.
You can't just use the index path.row (from & to) as these don't take account of the other sections.
I use the following method to get the correct rows:-
// Do not trigger delegate methods when changes are made to core data by the user
fetchedResultsController.delegate = nil
var fromIndex = fromIndexPath.row
var toIndex = toIndexPath.row
// print ("from row ",fromIndexPath.row)
//work out correct row to remove at based on it's current row and section
for sectionIndex in 0..<fromIndexPath.section
{
fromIndex += fetchedResultsController.sections![sectionIndex].numberOfObjects
}
//print ("fromIndex ",fromIndex)
// remove the row at it's old position
toDoData.remove(at: fromIndex)
//work out the correct row to insert at based on which section it's going to & row in that section
for sectionIndex in 0..<toIndexPath.section
{
toIndex += fetchedResultsController.sections![sectionIndex].numberOfObjects
//print ("toIndex ",toIndex)
if sectionIndex == fromIndexPath.section
{
toIndex -= 1 // Remember, controller still thinks this item is in the old section
//print ("-= toIndex",toIndex)
}
}
// put the item back into he array at new position
toDoData.insert(item, at: toIndex)
Note: toDoData is my array of records from the fetchedResultsController
Note: that you also have to update the section assuming you are keeping a record of it in core data and run through the array to update your order. use :-
fetchedResultsController.sections![toIndexPath.section].name
Related
I want to populate a UITableView in Swift 4 and I want to just show records that belong to the user that is logged in. Obviously I require an IF statement to see whether two values are equal to each other. How do I return null as such, i.e. no cell is added to the table. Please see the code below
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier:
"RunCell") as? RunCell else { return UITableViewCell() }
let user_id_main = String(MainMenuViewController.myVariables.user_id.prefix(2))
if (user_id_main==runs[indexPath.row].user_id)
{
cell.idLbl.text = "ID: " + runs[indexPath.row].run_id
cell.dateLbl.text = "Date: " + runs[indexPath.row].date_of_run
return cell
}
return cell
}
I know return cell outside of the IF still would return a cell, this is until I find an answer.
This is not the way UITableViews work. You can't decide on the fly if a cell is to be displayed or not. You need to do that before the table is loaded or reloaded.
Before the tableView is loaded or reloaded, filter your array into a new array that just contains records with the matching user_id. Then use this new array as the model for your table. The size of the new array will determine the number of rows in the table, and each row will correspond to one item of your new array.
Every time the user_id_main changes, refilter your runs array and reload the table.
you cannot return nil value cell,
to achieve what you want, first filter your runs array for the values you required and store in other array say mainUserRuns, then use this new array to provide data for your tableview.
I have a UITableViewCell and a BEMCheckBox and I want to delete the cell when the did tap is pressed here is my code :
func didTap(_ checkBox: BEMCheckBox) {
let index = tableView?.indexPath(for: self)
MenuViewController().products.removeObject(forKey: "Product\(index!.row)") // removes the cell from the database of the tableview
tableView?.deleteRows(at: [index!], with: .fade)
}
and here is how I get the tableView:
extension UIView {
func parentView<T: UIView>(of type: T.Type) -> T? {
guard let view = self.superview else {
return nil
}
return (view as? T) ?? view.parentView(of: T.self)
}
}
extension UITableViewCell {
var tableView: UITableView? {
return self.parentView(of: UITableView.self)
}
}
I get the follwing 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 (2) must be equal to the number of rows contained in that section before the update (2), 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).'
Its related to your data source.Your data source is not updated accordingly.You should check your data source before reloading UITableview. It happens due to the mismatch of data item count.
You need to notify the table view that you are about to modify the number of items:
tableView?.beginUpdates()
// Perform the item and cell removal
tableView?.endUpdates()
Always make sure to insert/delete the same number of items in your data source as the number of cells of your table view.
You can check the decoumentation here
I tried to delete a cell from collectionview on didSelect method.
Deleting the data is working well.
But I'm getting this:
reason: 'Invalid update: invalid number of sections. The number of
sections contained in the collection view after the update (1) must be
equal to the number of sections contained in the collection view
before the update (2), plus or minus the number of sections inserted
or deleted (0 inserted, 0 deleted).'
This is delete cell function:
func deleteMovie(cell: MoiveCollectionViewCell) {
var indexPath: NSIndexPath = self.collectionView!.indexPathForCell(cell)!
// remove and reload data
self.collectionView.deleteItemsAtIndexPaths([indexPath])
self.collectionView.reloadSections(NSIndexSet(index: indexPath.section))
}
and numberOfSectionsInCollectionView func :
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
//number_per_section = 3
if manager.movie_List.count % number_per_section == 0
{
return manager.movie_List.count / number_per_section
}
else {
return manager.movie_List.count / number_per_section + 1
}
}
I just want to reload number of sections. What should I add?
You need to update the datasource first before you make the calls to:
collectionView.deleteItemsAtIndexPaths([indexPath])
collectionView.reloadSections(NSIndexSet(index: indexPath.section))
So first delete the object in your datasource model (array, dictionary etc).
Then you can just do this:
let indexSet = IndexSet(integer: indexPath.section)
collectionView.reloadSections(indexSet)
The problem is you are just removing a cell from the section and that will definitely not work.
Once you have removed your cell you need to notify your manager.movie_List array too. So, remove the selectedIndex data from array and then try it out!
Is it possible to check the Parse column "ID" for 0 (any number) then if 0 (the one in the column) equals indexPath.row 0 (the first cell) it displays the data from the row of 0 in Parse? The picture above is the class for the viewcontroller, the tableview cell data is in another class.
ID: number that will be checked with the indexPath.row
navTitle: navigation bar title
articleTitle: UILabel
written: UILabel
date: UILabel
article: UITextView
Edit for possible duplicate: It is not a duplicate, the other question answers how to move data from a cell to a view controller using prepareForSegue. This question is asking how, if possible, to check a column on Parse and if a number in that column matches an indexPath.row it will use the data from that row to which the number corresponds.
I now understand you would like to show the data that is appropriate for a given index path in a detail view controller, after they tap on the cell at said index path. This is assuming you are correctly showing the appropriate data in the table view, and now the question is how to obtain and show the relevant data for the row the user tapped on in a new view controller.
To do this, you could add an internal property to your detail view controller that is an Int. Set this property to the table view's selected index path's row before the detail view controller is shown, perhaps in prepareForSegue for example.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let detailVC = segue.destinationViewController as? DetailViewController {
if let selectedIndexPath = self.tableView.indexPathForSelectedRow {
detailVC.integerIdentifier = selectedIndexPath.row
}
}
}
Now in that detail view controller, you can access the property's value and query for the information you need from Parse given that value. (And you'll likely want to display some feedback to the user to inform them you're downloading the appropriate data to display.) For example, you could have the following code in a function you call from viewDidLoad:
let query = PFQuery(className: "eventsDetail")
query.whereKey("ID", equalTo: self.integerIdentifier)
query.getFirstObjectInBackgroundWithBlock { (detail: PFObject?, error: NSError?) -> Void in
//update your labels etc using the detail (Optional) PFObject
}
I want to note, importantly, that this is not a great way to store the data in Parse. One should not tie the data to how it will be displayed, which is the case here tying an identifier to the index path, forcing item with ID 0 to magically be the article details appropriate for the first cell in the table. You may wish to revisit the database design. For example, perhaps your other class that is used to generate a list of events could have a pointer to the event detail class, which would allow you to obtain the event details before you select an event and not have to query again for details.
Original Answer:
I understand you would like to show the data in this Parse class in your app in a table view, where the first table view cell displays the data for ID 0, the second cell displays the data for ID 1, etc.
To accomplish this, you'll want to query this class and apply an ascending order on the "ID" column. You will get back an array of PFObjects that are sorted as desired - lowest to highest ID, which you can use for your table view's data source to map them one-to-one.
Your query may look something like this:
let query = PFQuery(className: "Article")
query.orderByAscending("ID")
query.findObjectsInBackgroundWithBlock { (articles: [PFObject]?, error: NSError?) -> Void in
//use articles array as the data source, update the interface
self.articles = articles
self.tableView.reloadData()
}
Now in your table view data source methods, use this array to populate the table:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.articles.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let article = self.articles[indexPath.row]
//... configure cell, show article title, etc, return cell
}
I am using a tableView to display a list of people. I am trying to add an alert to confirm that the user actually wants to delete the person and to prevent mistakes. However, when I try to delete the person that is stored with CoreData, there seems to be a problem reloading the view. I get this exception:
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 (2) must be equal to the number of rows contained in that section before the update (2), 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).'
Editing and Delete Function:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
var deleteRow = indexPath.row
indexPathforDelete = indexPath
let entityDescription = NSEntityDescription.entityForName("People", inManagedObjectContext: managedObjectContext!)
let request = NSFetchRequest()
request.entity = entityDescription
var error: NSError?
var objects = managedObjectContext?.executeFetchRequest(request, error: &error)
if let results = objects {
let personToDelete = results[deleteRow] as! NSManagedObject
let firstName = personToDelete.valueForKey("firstName") as! String
let lastName = personToDelete.valueForKey("lastName") as! String
var message = "Are you sure you would like to delete \(firstName) \(lastName)?\nThis will permanentaly remove all records of "
if(personToDelete.valueForKey("gender") as! String == "Male"){
message = "\(message)him."
}
else{
println(personToDelete.valueForKey("gender") as! String)
message = "\(message)her."
}
var deleteAlert : UIAlertView = UIAlertView(title: "Delete \(firstName) \(lastName)", message: message, delegate: self, cancelButtonTitle: "Cancel")
deleteAlert.addButtonWithTitle("Delete")
deleteAlert.show()
}
save()
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
AlertView Response Function:
func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int){
if(buttonIndex == 1){
managedObjectContext?.deleteObject(personToDelete)
tableView.deleteRowsAtIndexPaths([indexPathforDelete], withRowAnimation: .Fade)
save()
}
setEditing(false, animated: true)
self.navigationItem.leftBarButtonItem = nil
}
tableView number of rows function:
var personToDelete = NSManagedObject()
var indexPathforDelete = NSIndexPath()
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
let entityDescription = NSEntityDescription.entityForName("People", inManagedObjectContext: managedObjectContext!)
let request = NSFetchRequest()
request.entity = entityDescription
var error: NSError?
var objects = managedObjectContext?.executeFetchRequest(request, error: &error)
let results = objects
println("Results Count: \(results!.count)")
return results!.count
}
I think the problem is that you have two variables with the name propertyToDelete: a property that you declare and initialise with a blank NSManagedObject:
var personToDelete = NSManagedObject()
and a local variable that you declare within your commitEditingStyle function:
let personToDelete = results[deleteRow] as! NSManagedObject
It is this local variable to which you assign the object from your results array. But this local variable is destroyed when the function completes, and the AlertView action is deleting the object to which the property points. (The reason I hesitate is that I would expect your context to throw an error when it tries to delete an object that has never been registered with it). Note that by contrast you have only the one variable named indexPathforDelete. This holds the correct value when the AlertView action runs, and consequently the tableView deletes the correct row. That's why you get the error: it has deleted a row, but then finds (because no object has been deleted) it still has the same number of rows as before.
The immediate solution is to use the property within your function, rather than a local variable: just delete let:
personToDelete = results[deleteRow] as! NSManagedObject
But I would also recommend rethinking your approach: you are repeating the same fetch. If all the datasource methods do the same, it will be repeated numerous times when the table view is first built, whenever a cell is scrolled into view, whenever a cell is tapped, etc. This will be costly in terms of performance. You should instead undertake the fetch once (perhaps in viewDidLoad), store the results in an array property, and use that for the table view datasource methods. Alternatively, and perhaps preferably, use an NSFetchedResultsController: it is very efficient and there is boilerplate code for updating the table view when objects are added or deleted.
The documentations of tableView:commitEditingStyle:forRowAtIndexPath: says: "You should not call setEditing:animated: within an implementation of this method. If for some reason you must, invoke it after a delay by using the performSelector:withObject:afterDelay: method."