iOS 7 beginUpdates endUpdates inconsistent - ios

Edit:
The solution for this answer is related to iOS7 sometimes returning NSIndexPath and other times returning NSMutableIndexPath. The issue wasn't really related to begin/endUpdates, but hopefully the solution will help some others.
All - I'm running my app on iOS 7, and I'm running into problems with the beginUpdates and endUpdates methods for a UITableView.
I have a tableview that needs to change the height of a cell when touched. Below is my code:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// If our cell is selected, return double height
if([self cellIsSelected:indexPath]) {
return 117;
}
// Cell isn't selected so return single height
return 58;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
ChecklistItemCell *cell = (ChecklistItemCell *)[self.tableview cellForRowAtIndexPath:indexPath];
[cell.decreaseButton setHidden:NO];
[cell.increaseButton setHidden:NO];
// Toggle 'selected' state
BOOL isSelected = ![self cellIsSelected:indexPath];
DLog(#"%#", selectedIndexes);
DLog(#"is selected: %#", isSelected ? #"yes":#"no");
// Store cell 'selected' state keyed on indexPath
NSNumber *selectedIndex = #(isSelected);
selectedIndexes[indexPath] = selectedIndex;
[tableView beginUpdates];
[tableView endUpdates];
}
The beginUpdates and endUpdates methods are working pretty inconsistently. The didSelectRowAtIndexPath method gets called correctly on each touch(I thought at first the UI was getting blocked), and the selectedIndexes is storing alternating values correctly. The issue is, sometimes I touch a table cell and all the methods are called properly, but the cell height doesn't change. Anyone know what's going on?

There is a change in behavior in iOS7 where index paths are sometimes instances of NSIndexPath and other times UIMutableIndexPath. The problem is that isEqual between these two classes is always going to return NO. Thus, you cannot reliably use index paths as dictionary keys or in other scenarios that rely on isEqual.
I can think of a couple of viable solutions:
Write a method that always returns an instance of NSIndexPath and use it to generate keys:
- (NSIndexPath *)keyForIndexPath:(NSIndexPath *)indexPath
{
if ([indexPath class] == [NSIndexPath class]) {
return indexPath;
}
return [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
}
Identify rows by the data, not the index path. For example, if your data model is an array of NSString, use that string as the key into your selectedIndexes map. If your data model is an array of NSManagedObjects, use the objectID, etc.
I'm successfully using both of these solutions in my code.
EDIT Modified solution (1) based on #rob's suggestion of returning NSIndexPaths rather than NSStrings.

endUpdates shouldn't be called immediately after beginUpdates. The latter's documentation states, "Begin a series of method calls that insert, delete, or select rows and sections of the receiver." That suggests that it should be called in willSelectRowAtIndexPath: and endUpdates should be called in didSelectRowAtIndexPath.

Related

didDeselectRowAtIndexPath issue

I have a tableview in a scrollview in a popover. When the view is presented, the bottom cell in tableview is not visible to the user. If I select all of the cells then deselect the fist cell, the out of view cell is deselected too. Has anyone come across this behaviour before? If so, how to approach it?
Now your job is to find all the visible cells in the tableview and then apply select/deselect to it.
UITableView *tableView = self.tableView;
// Or however you get your table view
NSArray *paths = [tableView indexPathsForVisibleRows];
// For getting the cells themselves
NSMutableSet *visibleCells = [[NSMutableSet alloc] init];
for (NSIndexPath *path in paths)
{
[visibleCells addObject:[tableView cellForRowAtIndexPath:path]];
}
// Now visibleCells contains all of the cells you care about.
-(void) tableView:(UITableView *)tableView didDeselectRowAtIndexPath:
(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:
(NSIndexPath *)indexPath {
//stuff
//as last line:
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
For that matter, deselectRowAtIndexPath can be called from anywhere at any time you want the row to be deselected.
[self.myTableView deselectRowAtIndexPath:[self.myTableView
indexPathForSelectedRow] animated: YES];
If you are using dequeueReusableCellWithIdentifier: change your cellForRowAtIndexPath: to use dequeueReusableCellWithIdentifier:forIndexPath:
In a UITableView cells get reused. That means it only produces as many as absolutely needed. As soon as a new one is coming onto the screen, the last one is "recycled" instead of initialising a whole new instance.
This makes your application run faster. It also means that you have to undo any changes you made, when recycling.
Selection status is one of them. The UITableView should manage this automatically for you, if it is dequeued with the relevant indexPath. If not, it wouldn't know whether that specific cell should be selected.

Validity of the UITableView Cells in ViewWill Appear

What I have
1). Container has UITableView, which has two custom UITableViewCells.
2). Core Data has certain entity which has a text to be displayed at
UITableViewCell each time I get into the View.
What i am doing ?
1) I have chosen -viewWillAppear method which gets invoked each time the view is visible.
2) In -viewWillAppear, I retrieved the data from core data.
3) Retrieved particular cell from UITableView
NSUInteger idxArr[] ={2,0}; // 2 nd section, 0th Row.
NSIndexPath *cPath = [NSIndexPath indexPathWithIndexes:idxArr length:2];
myCell *tCell = (myCell *)[self.settings cellForRowAtIndexPath:cPath];
tCell.myLabel.text = rec.servername; // rec.servername is from DC.
When I checked in the lldb,
tCell was nil.
Questions:
1) It is the right way of getting the Cell ?
2) Or, By the time -viewWillAppear, does the UITableView not Ready ?
I am sure.
You should populate the cells by conforming to tableView dataSource protocol and then in your viewWillAppear you should call reloadData on your tableView.
After calling reloadData for tableview, We need to call -scrollToRowAtIndexPath: before getting cell from -cellForRowAtIndexPath:.
Because, As we are calling a row in section 2, it might not be in the visible area until we scroll. So, cellForRowAtIndexPath: returns nil.
Method -cellForRowAtIndexPath: shouldn't be called programically. It's a data source method for UITableView and it contain some cell reuse optimalizations. If you update the view after scrolling down and up -tableView:cellForRowAtIndexPath will be called again and your changes won't be visible.
If you want to update specific cell you should update make changes in:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
YourCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CellId" forIndexPath:indexPath];
YourData *data = //Get your data here
if (data.isReady) {
cell.tf.text = data[indexPath.row].text;
} else {
cell.tf.text = #"Not ready yet. Need to reload this cell later";
}
return cell;
}
And then call method below when you finish fetch your data.
[self.tableView reloadRowsAtIndexPaths:(NSArray *) withRowAnimation:UITableViewRowAnimationFade];
If you want to reload whole tableView (usually it's not slow) as #salaman140 says you can call [self.tableView reloadData] to update all visible cells.
If I were you I wouldn't use:
NSUInteger idxArr[] ={2,0}; // 2 nd section, 0th Row.
NSIndexPath *cPath = [NSIndexPath indexPathWithIndexes:idxArr length:2];
I would (is much more clear):
NSIndexPath *cPath = [NSIndexPath indexPathForRow:0 inSection:2];

Terminating with uncaught exception of type NSException when deleting row from UITableView

I'm working on an app that will update and add user's coordinate to a UITableView when the user has traveled over a preset distance interval. The coordinates will be automatically added to an NSMutableArray, and I use the array to update the table.
Everything load up and work fine (I can edit the table by moving and re-ordering the rows) but whenever I chose to delete a specific row, the program crash with the error "libc++abi.dylib: terminating with uncaught exception of type NSException".
- (void) tableView:( UITableView *) tableView commitEditingStyle:( UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:( NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSArray *items = [[G5SharedStore sharedStore]allCoords];
G5SharedStore *item = items[indexPath.row];
[[G5SharedStore sharedStore]removeItem:item];
//THIS IS WHERE THE ERROR OCCURS
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
The last line is what causes the error, but I'm new to objective-c, so I'm not sure how to fix this. Any help would be appreciated. Let me know if you need more details.
Thanks in advance.
**Edit:
Ok, I played around with the code this morning and it works. The problem is that the array keeps adding more and more items, but the tableview doesn't, unless I go back out and click "show table" button again to refresh it. So whenever I delete something in the tableview, the table's size is inconsistent with the array's size therefore I get the error. Here's my new problem, I tried to solve the above problem by making the table automatically update its data using:
[tableView reloadData];
The table does update, but it keeps adding blank cells ... with no data in it. Here's where I added the above:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#" UITableViewCell"
forIndexPath:indexPath];
NSArray *items = [[G5SharedStore sharedStore] allCoords];
G5SharedStore *item = items[indexPath.row];
cell.textLabel.text = [item description];
[tableView reloadData];
return cell;
}
Since I just started iOS programming 1 month ago, I could be wrong. So please guide me.
Thanks again everyone
I can see two problems:
Firstly this article recommends modifying the table view before the data-model (you are doing it the other way round):
It must do two things:
Send deleteRowsAtIndexPaths:withRowAnimation: or insertRowsAtIndexPaths:withRowAnimation: to the table view to direct
it to adjust its presentation.
Update the corresponding data-model array by either deleting the referenced item from the array or adding an item to the array.
Secondly you don't appear to be calling beginUpdates and endUpdates around that call. To quote the reference:
Note the behavior of this method when it is called in an animation
block defined by the beginUpdates and endUpdates methods.
Instead of delete row delete object from the array and reload table
- (void) tableView:( UITableView *) tableView commitEditingStyle:( UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:( NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSArray *items = [[G5SharedStore sharedStore]allCoords];
G5SharedStore *item = items[indexPath.row];
[[G5SharedStore sharedStore]removeItem:item];
//Remove this
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
//Add This Line
[tableView reloadData];
}
}

UITableViewCell delete button not disappearing

I'm using a UISegmentedControl to switch a UITableView between two datasets (think favorites and recents). Tapping the segmented control reloads the tableview with the different data set.
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:anim];
When the user swipes to delete a row it works fine. HOWEVER when the user switches datasets via the segmented control, the DELETED CELL gets re-used without altering it's appearance (i.e. the red 'DELETE' button is still there and the row content is nowhere to be seen). This appears to be the opposite problem that most people are seeing which is the delete button not appearing.
This is the delete code:
- (UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewCellEditingStyleDelete;
}
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
if ([self.current isEqualTo:self.favorites])
{
Favorite *fav = self.favorites[indexPath.row];
NSMutableArray *mut = [self.favorites mutableCopy];
[mut removeObjectAtIndex:indexPath.row];
self.favorites = mut;
self.current = self.favorites;
[self.tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}
The tableview is set to single select, and self.tableView.editing == NO. I have also tried using [self.tableView reloadData] and deleting/inserting the difference in rows from one dataset to the next. Neither works.
The UITableViewCell I'm using supplies no backgroundView or selectedBackgroundView
[EDIT]
Segmented Control Value Changed:
- (IBAction)modeChanged:(id)sender
{
if (self.listMode.selectedSegmentIndex == 1)
{
self.current = self.favorites;
}
else
{
self.current = self.recents;
}
// Tryin this:
[self.tableView reloadData];
// Tried this:
// [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
// Only 1 Section per table
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
{
return [self.current count];
}
Oh for the love of...
I wasn't calling [super prepareForReuse]; in my UITableViewCell subclass.
UGH.
I ran into the same thing: to "delete" a custom UITableViewCell, I was removing it from the table and putting it onto another list, which the user could then display in a modal view when they have regrets and want to put it back. In iOS7 (but not iOS6), the cells so moved had the big ugly "DELETE" button still on them, despite calling setEditing:NO and so on. (And in addition, the rest of the cell content was not drawn at all, even though inspecting the cells in the debugger showed that all the subpanes were still there.)
Unlike Stephen above, I hadn't overridden prepareForReuse, so that wasn't the problem. But it was related: in my case, the cells weren't created with a reuse identifier:
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
And per the docs, "If the cell object does not have an associated reuse identifier, this method is not called." But apparently, in iOS7 at least, it should be.
So the solution, in my case, was to explicitly call this [cell prepareForReuse] on each cell as I loaded it into the new table.

When tapping an already selected cell in a UITableView, willSelectCellAtIndexPath gets called

I have a UITableView with two sections. Based on user interactions (selections and deselections), my datasource and UITableView are updated to move data between sections. Initially their is only data in section 0. When I tap a cell, willSelectCellAtIndexPath and didSelectCellAtIndexPath get called. As expected, when I tap the same cell again, didDeselectCellAtIndexPath is called.
Even after I begin to move data down to section 1 and select and deselect, the UITableView's delegate methods are called appropriately.
Once all data has been moved to Section 1, the UITableView begins to exhibit strange behavior. I can initially select a call and didSelectCellAtIndexPath is called. However, when I tap it again, didDeselectCellAtIndexPath is never called. Instead, any taps on the selected cell (I have confirmed it is indeed selected through [tableView indexPathsForSelectedCells] or any other cells in Section 1 only result in willSelectIndexPath and didSelectIndexPath getting called.
I have quite a bit of code in these delegate methods which is unrelated (I believe).... I do not explicitly change the selected state of a cell anywhere. I have posted willSelect method and can post more if necessary.
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (remainingItemIsSelected && indexPath.section == 0) {
//other cells in the remaining items section are selected and a cell from that section is being selected
NSMutableIndexSet *arrayIndexesToBeDeleted = [[NSMutableIndexSet alloc] init];
for (NSIndexPath *previouslySelectedIndexPath in [tableView indexPathsForSelectedRows]) {
if (((ReceiptItem *)[self.remainingReceiptItems objectAtIndex:previouslySelectedIndexPath.row]).allocated == YES) {
//update data sources
NSLog(#"%#%i%#,%i",#"Section #:",previouslySelectedIndexPath.section,#" Row #:",previouslySelectedIndexPath.row);
[self.assignedReceiptItems addObject:[self.remainingReceiptItems objectAtIndex:previouslySelectedIndexPath.row]];
[arrayIndexesToBeDeleted addIndex:previouslySelectedIndexPath.row];
//update index path arrays
[self.receiptItemsToDeleteIndexPaths addObject:previouslySelectedIndexPath];
[self.receiptItemsToAddIndexPaths addObject:[NSIndexPath indexPathForRow:self.assignedReceiptItems.count-1 inSection:1]];
//update the pressed indexpath to equal to resulting indexpath to pass on to the didSelect method
if (previouslySelectedIndexPath.row < indexPath.row) {
indexPath = [NSIndexPath indexPathForRow:indexPath.row-1 inSection:0];
}
}
}
//Delete assigned items from the remaining receipt items
[self.remainingReceiptItems removeObjectsAtIndexes:arrayIndexesToBeDeleted];
//update table (move allocated item down)
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:self.receiptItemsToDeleteIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:self.receiptItemsToAddIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
if (self.remainingReceiptItems.count == 0) {
[tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
}
//other cells in the remaining items section are selected and a cell from assigned items is being selected
return nil;
}
return indexPath;
}
From the Documentation for UITableViewDelegate:
tableView:willDeselectRowAtIndexPath:
This method is only called if there is an existing selection when the user tries to select a different row. The delegate is sent this method for the previously selected row.
If you think this through you will find that what you encounter is expected behavior. Tapping a row that is selected does not call will/didDeselctRowAtIndexPath on this row.
Instead, you could handle this in didSelectRowAtIndexPath for the selected row, i.e. deselect it there.
Possible Alternative
That being said, I think you are abusing the UITableView class. It is really not designed to do this moving stuff. You have no doubt noticed yourself that you have to write a lot of code to make this work -- the very reason you are encountering intractable errors.
It seems to me that a much cleaner (and ultimately more flexible) solution would be to have two separate table (or other) views that notify each other via delegates about datasource changes. Maybe a bit more work setting it up, but surely much less trouble down the road.

Resources