Delete cell from UITableView not from Array - objective-c

I learn how work with CoreData and Table view controller. I learn from a book and there is using this function for display content of TableViewControoler.
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
if(debug == 1){
NSLog(#"Running %# '%#'",self.class, NSStringFromSelector(_cmd));
}
static NSString *cellIndetifier = #"Class Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIndetifier
forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryDetailButton;
SchoolClass *class = [self.frc objectAtIndexPath:indexPath];
NSMutableString *title = [NSMutableString stringWithFormat:#"%#", class.name];
[title replaceOccurrencesOfString:#"(null)"
withString:#""
options:0
range:NSMakeRange(0, [title length])];
cell.textLabel.text = title;
return cell;
}
Now I would like delete a record(cell) from table view controller and from coreData. I don't know what is the simplest way, but I found this:
I have one button "delete" and the button has this IBA action.
-(IBAction)deleteClassAction:(id)sender{
[self.tableView setEditing:YES animated:YES];
}
When I press the button I see something like this:
http://i.stack.imgur.com/3VAyp.png
Now I need delete the selected cell. I found some code and I prepared skeleton for deleting. But I need advice in two things.
I don't know how get the ID of deleting item and how to write code for filter - viz. comment: // the code that filters the object that has been marked for deleting cell from tableView
I don't know how delete the cell from tableView. I found the solution, which is under the comment, but it has a 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 (10) must be equal to the number of rows contained in that section before the update (10), 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).'
Here is the code with comments.
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
// deleting data from CoreData database
CoreDataHelper *cdh = [(AppDelegate *) [[UIApplication sharedApplication] delegate] cdh];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"User"];
NSPredicate *filter = // the code that filters the object that has been marked for deleting cell from tableView"
[request setPredicate:filter];
NSArray *fetchedObjects = [cdh.context executeFetchRequest:request error:nil];
for(SchoolClass *class in fetchedObjects){
[_coreDataHelper.context deleteObject:class];
}
// deleting cell from tableView
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
[self.tableView reloadData];
}

You just need to delete the object from your NSFetchedResultsController. You might also then want to save the NSManagedObjectContext. The fetched results controller will then update the table view assuming you've implemented the delegate methods as described in the docs.
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
// deleting cell from fetched results contoller
if (editingStyle == UITableViewCellEditingStyleDelete) {
SchoolClass *class = [self.frc objectAtIndexPath:indexPath];
[[class managedObjectContext] deleteObject:class];
}
}

Related

fetchedresultscontroller delegate called with invalid indexpath when 1 managed object updated and another deleted [duplicate]

I have a UITableView that uses an NSFetchedResultsController as data source.
The core data store is updated in multiple background threads running in parallel (each thread using it's own NSManagedObjectContext).
The main thread observes the NSManagedObjectContextDidSaveNotification
notification and updates it's context with mergeChangesFromContextDidSaveNotification:.
Sometimes it happens that the NSFetchedResultsController sends an
NSFetchedResultsChangeUpdate event with an indexPath that does not exist
anymore at that point.
For example: The result set of the fetched results controller contains
1 section with 4 objects. The first object is deleted in one thread.
The last object is updated in a different thread. Then sometimes the
following happens:
controllerWillChangeContent: is called.
controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is called with
type = NSFetchedResultsChangeDelete, indexPath.row = 0.
controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is called with
type = NSFetchedResultsChangeUpdate, indexPath.row = 3.
But the fetched results controller contains only 3 objects now, and if call
MyManagedObject *obj = [controller objectAtIndexPath:indexPath]
to update the table view cell according to the NSFetchedResultsChangeUpdate
event, this crashes with a NSRangeException exception.
Thank you for any help or ideas!
I have now found a solution for my problem. In the case of an update event, there is no need to call
[self.controller objectAtIndexPath:indexPath]
because the updated object is already supplied as the anObject parameter to the -controller:didChangedObject:... delegate.
I have therefore replaced -configureCell:atIndexPath: by a -configureCell:withObject: method that uses the updated object directly. This seems to work without problems.
The code looks now like this:
- (void)configureCell:(UITableViewCell *)cell withObject:(MyManagedObject *)myObj
{
cell.textLabel.text = myObj.name;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyCellIdentifier"];
[self configureCell:cell withObject:[self.controller objectAtIndexPath:indexPath]];
return cell;
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
/* ... */
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject];
break;
/* ... */
}
}
This is actually quite common because of the bug in Apple's boiler plate code for NSFetchedResultsControllerDelegate, which you get when you create a new master/detail project with Core Data enabled:
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
Solution #1: Use anObject
Why query the fetched results controller and risk using an incorrect index path when the object is already given to you? Martin R recommends this solution as well.
Simply change the helper method configureCell:atIndexPath: from taking an index path to take in the actual object that was modified:
- (void)configureCell:(UITableViewCell *)cell withObject:(NSManagedObject *)object {
cell.textLabel.text = [[object valueForKey:#"timeStamp"] description];
}
In cell for row, use:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[self configureCell:cell withObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
return cell;
}
Finally, in the update use:
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
withObject:anObject];
break;
Solution #2: Use newIndexPath
As of iOS 7.1, both indexPath and newIndexPath are passed in when a NSFetchedResultsChangeUpdate happens.
Simply keep the default implementation's usage of indexPath when calling cellForRowAtIndexPath, but change the second index path that is sent in to newIndexPath:
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:newIndexPath];
break;
Solution #3: Reload rows at index path
Ole Begemann's solution is to reload the index paths. Replace the call to configure cell with a call to reload rows:
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
There are two disadvantages with this method:
By calling reload rows, it will call cellForRow, which in turn calls dequeueReusableCellWithIdentifier, which will reuse an existing cell, possibly getting rid of important state (e.g. if the cell is in the middle of being dragged a la Mailbox style).
It will incorrectly try and reload a cell that isn't visible. In Apple's original code, cellForRowAtIndexPath: will return "nil if the cell is not visible or indexPath is out of range." Therefore it would be more correct to check with indexPathsForVisibleRows before calling reload rows.
Reproducing the bug
Create a new master/detail project with core data in Xcode 6.4.
Add a title attribute to the core data event object.
Populate the table with several records (e.g. in viewDidLoad run this code)
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
for (NSInteger i = 0; i < 5; i++) {
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
[newManagedObject setValue:[#(i) stringValue] forKey:#"title"];
}
[context save:nil];
Change configure cell to show the title attribute:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:#"%# - %#", [object valueForKey:#"timeStamp"], [object valueForKey:#"title"]];
}
In addition to adding a record when the new button is tapped, update the last item (Note: this can be done before or after the item is created, but make sure to do it before save is called!):
// update the last item
NSArray *objects = [self.fetchedResultsController fetchedObjects];
NSManagedObject *lastObject = [objects lastObject];
[lastObject setValue:#"updated" forKey:#"title"];
Run the app. You should see five items.
Tap the new button. You will see that a new item is added to the top, and that the last item does not have the text "updated," even though it should have had it. If you force the cell to reload (e.g. by scrolling the cell off the screen), it will have the text "updated."
Now implement one of the three solutions outlined above and in addition to an item being added, the last item's text will change to "updated."

Delete from UITableView using an NSFetchedResultsController

I have a UITableView which is populated through a NSFetchedResultsController. When the user slides an item to the right I have a delete button appearing so that he/she can remove the object using the following approach:
// Override to support conditional editing of the table view.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
//perform similar delete action as above but for one cell
XMPPUserCoreDataStorageObject *user = [[self fetchedResultsController] objectAtIndexPath:indexPath];
NSLog(#"User delete: %#", [user displayName]);
//delete from fetchController
NSArray *sections = [[self fetchedResultsController] sections];
int userStatus = [[user sectionNum] intValue];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
But when I do that there is an exception because I am not updating the model which is my fetchedResultsController. The question is how to remove from the fetchedController using the indexPath that I have from the commitEditingStyle ?
Since you are using NSFetchedResultsController you only need to delete the object from the context, and it will pick up the change and delete the rows for you.
So remove the deleteRowsAtIndexPaths: line and call this instead:
[self.fetchedResultsController.managedObjectContext deleteObject:user];
Have a look at this Answer , what you have to do is delete an NSManagedObject from your database as well as from the NSFetchedResultsController.

Add / Delete Item from NSMutableDictionary through UITableView

I've got a .plist file with NSMutableDictionary that I am populating a UITableView like so:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *myFile = [[NSBundle mainBundle]
pathForResource:#"courses" ofType:#"plist"];
courses = [[NSMutableDictionary alloc] initWithContentsOfFile:myFile];
courseKeys = [courses allKeys];
}
And:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Course";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
NSString *currentCourseName = [courseKeys objectAtIndex:[indexPath row]];
[[cell textLabel] setText:currentCourseName];
return cell;
}
I am trying to allow the user to swipe + delete and also "add" new items to the plist using the UiTableView cells. This is the code I'm using to do this:
- (void)tableView:(UITableView *)tableView commitEditingStyle: (UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
When I run the simulator and do this it gives me an error, stating
"Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (7) must be equal to the number of rows contained in that section before the update (7), 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).'"
Can anyone help me out here?
You are populating your table from the courseKeys Array. So in your delegate method
- (void)tableView:(UITableView *)tableView commitEditingStyle: (UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
//whether it is an add or edit or delete modify your array from which you are populating the table and reload the table
}
You are removing the row from the table but not from your data source. CourseKeys needs to be a mutableArray that you remove deleted keys from.

iOS - NSRangeException only when breakpoints are disabled

Recently started developing apps, so excuse my ignorance. I have a tableView, and when a cell in the table view is clicked, I want to insert a new row directly below it. This currently works in my code. However, I also want the row that has been inserted to be removed once the cell has been clicked again. This is giving me the NSRangeException saying I am out of bounds in my array.
I figured this probably has to do with my tableView delegate/data methods, so I set up break points at each of them. With the break points enabled, the cell is removed perfectly. However, when I disable the breakpoints, and let the application run on its own, it crashes. How could break points possibly be affecting this?
Here is the relevant code:
- (NSInteger) numberOfSectionsInTableView:(UITableView *)songTableView{
return 1;
}
- (NSInteger)tableView:(UITableView *)songTableView
numberOfRowsInSection:(NSInteger)section{
bool debug = false;
if (debug) NSLog(#"--TableView: rankings");
if (expandedRow == -1)
return [self.songs count];
else //one row is expanded, so there is +1
return ([self.songs count]+1);
}
- (UITableViewCell *)tableView:(UITableView *)songTableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
bool debug = false;
if (debug) NSLog(#"--tableView: tableView");
NSUInteger row = [indexPath row];
if (row == expandedRow){ //the expanded row, return the custom cell
UITableViewCell *temp = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"test"];
return temp;
}
UITableViewCell *cell = [tableViewCells objectAtIndex:row];
return cell;
}
}
- (NSString *)tableView:(UITableView *)songTableView
titleForHeaderInSection:(NSInteger)section{
//todo: call refresh title
return #"The Fresh List";
}
- (CGFloat)tableView:(UITableView *)songTableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 44.0; //same as SongCell.xib
}
- (void)tableView: (UITableView *)songTableView
didSelectRowAtIndexPath: (NSIndexPath *)indexPath {
bool debug = true;
//todo: if the user selects expanded cell, doesn't do anything
SongCell *cell = (SongCell *)[songTableView cellForRowAtIndexPath:indexPath];
if (cell->expanded == NO){
//change cell image
cell.bgImage.image = [UIImage imageNamed:#"tablecellbg_click.png"];
cell->expanded = YES;
//add new cell below
NSInteger atRow = [indexPath row] + 1;
NSIndexPath *insertAt = [NSIndexPath indexPathForRow:atRow inSection:0];
NSArray *rowArray = [[NSArray alloc] initWithObjects:insertAt, nil];
if (debug) NSLog(#"Expanded row: %d", atRow);
expandedRow = atRow;
[tableView insertRowsAtIndexPaths:rowArray withRowAnimation:UITableViewRowAnimationTop];
}else { //cell is already open, so close it
//change cell image
cell.bgImage.image = [UIImage imageNamed:#"tablecellbg.png"];
cell->expanded = NO;
NSIndexPath *removeAt = [NSIndexPath indexPathForRow:expandedRow inSection:0];
NSArray *rowArray = [[NSArray alloc] initWithObjects:removeAt, nil];
if(debug) NSLog(#"--about to delete row: %d", expandedRow);
expandedRow = -1;
[tableView deleteRowsAtIndexPaths:rowArray withRowAnimation:UITableViewRowAnimationTop];
//remove expaned cell below
}
}
This is just a guess, but it's a good idea to wrap code that changes the table structure in calls to
[tableView beginUpdates];
[tableView endUpdates];
I bet this returns null: [NSIndexPath indexPathForRow:expandedRow inSection:0]; and if it does it blows ...
hth
I hate to answer my own questions but I figured it out.
I was loading up my tableView from an array of objects. When I added the cell, that array still only held 30 objects, whereas my table held 31. I was correctly returning the numberOfRowsInSection method.
Here is the modification I made. Notice the extra else if:
NSUInteger row = [indexPath row];
if (row == expandedRow){ //the expanded row, return the custom cell
if(debug) NSLog(#"row == expandedRow");
UITableViewCell *temp = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"test"];
return temp;
}
else if (expandedRow != -1 && row > expandedRow)
row--;
My array of objects and the UITableViewCells were suppose to match up 1-1. After the expanded row, indexPath's row because off by 1. Here is my quick fix to this problem, although I'm sure there is a better way to solve this.

TableView returns null (0) when selecting row

I have a TableView control which I've sectioned/grouped. However this has now broken the code which determines the item I've selected in the Table.
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
/*
When a row is selected, set the detail view controller's detail item to the item associated with the selected row.
*/
//detailViewController.detailItem = [NSString stringWithFormat:#"Row %d", indexPath.row];
if (_delegate != nil) {
Condition *condition = (Condition *) [_conditions objectAtIndex:indexPath.row];
[_delegate conditionSelectionChanged:condition];
}
}
My sample data currently has 6 sections, the first one contains two items with all the rest containing one. If I select the second item in the first section, the details view updates fine, any other selection always shows the first item in the first section (item 0).
I'm assuming this is because indexPath.row is returning 0 for the other sections as it's the first item in that section, which is then causing the first object from my array to be chosen.
How do I fix my code to ensure the correct item is selected? Everything else works fine other than this.
cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"CellIdentifier";
// Dequeue or create a cell of the appropriate type.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryNone;
}
//get the letter in the current section
NSString *alphabet = [conditionIndex objectAtIndex:[indexPath section]];
//get all conditions beginning with the letter
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name beginswith[c] %#", alphabet];
NSArray *conditionsGrouped = [_conditions filteredArrayUsingPredicate:predicate];
if ([conditionsGrouped count] > 0) {
Condition *condition = [conditionsGrouped objectAtIndex:indexPath.row];
cell.textLabel.text = condition.name;
}
return cell;
}
You should take into account indexPath.section, as well.
You should create a bi-dimensional _conditions array, indexed by section and then by row.
Got it working by following the answer at this post:
UITableView Problem with Sections