I've implemented thedidDeselectRowAtIndexPath method in a UITableView which includes multiple selection.
For some reason, didDeselectRowAtIndexPath is not being called by the delegate. Any suggestions? (I haven't accidentally misspelled didSelectRowAtIndexPath).
Thanks!
Some things to note about cell selection:
didDeselectRowAtIndexPath is only called in a single-selection UITableView if the user clicks a different cell than the one already selected.
didSelectRowAtIndexPath is called to select the new row, and didDeselectRowAtIndexPath is called to deselect the previous row
In a UITableView with multiple-selection, the previous row is not being deselected, so your deselect is handled in didSelectRowAtIndexPath
You can grab the cell using the delegate method you've implemented and modify it after checking it's selection like so:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell.selected) {
// ... Uncheck
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
}
I currently use a tableView with multiple selection, and didDeselectRowAtIndexPath method is calling for me (unlike what is stated in third bullet point in answer above).
I did note that cells that were "preselected", did not call didDeselectRowAtIndexPath correctly unless I did three things:
In cellForRowAtIndexPath, when I find a cell I want to preselect (you might check for matching array entries, or a string like I did), set cell.selected = true for that cell
In cellForRowAtIndexPath, also make a call to method
selectRowAtIndexPath for the cell to preselect
In Interface Builder, uncheck the "Show Selection on Touch" checkmark for the tableView. Oddly enough, this was required to ensure that when a preselected item was tapped (ie should be deselected), that didDeselectRowAtIndexPath was called correctly.
Example code:
if productsReceived!.rangeOfString(cell.productName.text!) != nil {
cell.accessoryType = .Checkmark
tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: UITableViewScrollPosition.None)
cell.selected = true
}
I hope this helps someone who is trying to implement multiple selection, with preselected cells.
If I'm not mistaken, UITableView doesn't have a built in method for deselecting cells. I believe you need to call deselectRowAtIndexPath: in order for didDeselectRowAtIndexPath to be called.
Related
What is the best way of preventing the user from selecting a cell inside a UITableView, but allowing my program to call selectRowAtIndexPath: on the table view?
I also want the controls in the UITableViewCell to remain interactive (i.e. allow touchesBegan: to be called on the UITableViewCell).
If I do [tableView setAllowsSelection:NO], calling selectRowAtIndexPath: does not do anything.
I realized that when you call selectRowAtIndexPath: programmatically, then the delegate method willSelectRowAtIndexPath: will not be called. However, if a user taps a cell, then this will be called. This method can return nil to prevent selection.
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
return nil;
}
This will ensure that only programmatic calls to selectRowAtIndexPath: will select a row and any taps to select a row will not.
Uncheck the box : User Interaction Enabled in TableView as well as TableViewCell
I'm trying to understand why awakeFromNib is being called twice in my code. I currently have a tableview that has a special compressible cell that appears once at the end of the table. The first awakeFromNib is being called when the tableview is scrolled to the special cell at the end (which is fine I believe,as the tableview is reusing cells). However, whenever I tap the cell to expand the cell, the awakeFromNib is being called again.
Could anyone explain to me why awakeFromNib is being called twice? And how I could only make it only be called once?
Thanks
EDIT** Code people have requested
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section >= (NSInteger)[self.trip.destinations count]) {
GuestCell *cell = (GuestCell*)[tableView dequeueReusableCellWithIdentifier:GuestCellIdentifier forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell setupCellForGuests:self.trip.guests];
cell.guestExpanded = NO;
NSLog(#"RETURNING CELL");
return cell;
}
// For all other sections
return [self prepareCardCellForIndexPath:indexPath forHeightCalc:NO];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section >= (NSInteger)[self.trip.destinations count]) {
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
You're animating the reload of the expanding row. The table view implements this by creating another cell for the same index path, and animating a transition from the old cell to the new cell. It creates a second instance of your cell prototype, so the second instance also receives the awakeFromNib message. If you log self, you'll see that the address is different the second time.
I don't think you can avoid the creation of a second cell instance (and thus a second awakeFromNib) unless you get rid of the animation. Even then I'm not sure it will reuse the old cell.
If the cell with that nib is only one in the table, then my guess is that it has something to do with animations. I didn't check how tableview handles cells during animation, but for tableview header it asks for another instance and then performs animation (for example fade) - so the old instance is faded out and the new is faded in. At least that's what I think has the highest probability, if you are handling cells correctly.
I have the following UITableViewCell (well, subclassed).
With didSelectRowAtIndexPath it is possible to capture that a cell has been selected in UITableViewController. My problem occurs due to the fact that directly pressing Choose User bypasses the selection of the cell.
How could I allow my UITableViewController to be aware that UITableViewCell foo has been pressed even if the user immediately hits Choose User?
N.B. I don't need the Selection capability per se, this was just by method of knowing that a user had tapped within a cell area.
You could just call the method directly. If we say that for each Choose User button we are setting the row number as the tag and assuming that you don't have sections so everything will happen in section 0 we could do.
- (void)hitChooseUser:(id)sender
{
// Do whatever you want for when a user hits the `Choose User` button
// Code......
// Then do this at the end or whenever you want to do it.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[sender tag] inSection:0];
// Also assuming you have created you have created you UITableView correctly.
[self tableView:myTableView didSelectRowAtIndexPath:indexPath];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Do whatever it is you want.
}
I also found this link that may help you Manually call didSelectRowatIndexPath
You could also disable the user interaction with the cell itself by setting userInteractionEnabled: to NO for each cell in cellForRowAtIndexPath: so didSelectRowAtIndexPath: will only get called when you want to call it manually.
Do not call didSelectRowAtIndexPath: It is a UITableViewDelegate method and, where possible, should be used as such (meaning let the UITableView send messages to it). In addition, it creates an unnecessary dependency on UITableView implementation.
That being said, in order to achieve shared behavior that is performed either on button click, or on row selection, refactor it out into a common method that is not coupled with UITableViewDelegate
For example:
-(void)doSomethingCommon {
//do shared code here
}
-(void)chooseUserButtonPressed:(id)sender {
[self doSomethingCommon];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self doSomethingCommon];
}
And if your UITableView shows more than one of these rows, for which you depend on knowing which corresponding model object is related to the cell, than you can use the tag property on UIView subclasses (usually something in your cell) to mark the row that the object is shown in.
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.
I have a UITableViewController with a segue where I'm trying to get the currently selected row:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"EditFilterElementSegue"]){
// Get the element that was clicked and pass it to the view controller for display
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
Element *element = [fetchedResultsController objectAtIndexPath:selectedIndexPath];
FilterElementTableViewController *vc = segue.destinationViewController;
vc.filter = filter;
vc.element = element;
}
}
The problem is indexPathForSelectedRow is returning nil. The docs say nil is returned "if the index path is invalid". If I add:
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:YES scrollPosition:0];
selectedIndexPath = [self.tableView indexPathForSelectedRow];
selectedIndexPath is valid. So, I'm currently assuming the row is somehow getting unselected. Can anyone tell me how to figure out if this is the case and what might be causing it?
This code worked fine until I converted to using NSFetchedResultsController (which otherwise is working). I also use indexPathForSelectedRow in several other table view controllers where it works fine (one of which also uses NSFetchedResultsController).
I've also tried breaking in didSelectRowAtIndexPath, but the same thing happens there. (expected since it's called after prepareForSegue.)
A possible cause is that you've written something in one of the UITableViewDelegate methods -
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
}
It's a common practice to deselect the row which has just been selected.
Update:
In Swift 4.2, xcode 10:
Just comment the line
tableView.deselectRow(at: indexPath, animated: true)
in the function
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
Something that may help others - if you call reloadData() on your table view, it nilifies the indexPathForSelectedRow.
Be sure, you are not calling deselectRowAtIndexPath method before getting index path of selected Row.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES]; //add this line after setting indexPath
}
Ok In my case, I was calling from UIButton in storyboard by show segue.
And removing following line:
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
with below 2 lines worked for me:
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
hope with my case "calling segue from UIButton on CELL" works for others also.
Ok, the bottom line is the scene in storyboard was messed up.
To figure it out I removed the segue and created a brand new empty UITableViewController class and only added enough code so I could click on a row and look at indexPathForSelectedRow. It was still nil.
I then created a new UITableViewController in storyboard and replaced the existing one. Now indexPathForSelectedRow worked.
I copied all the class code over, and the prototype cells over, and it still worked!
Out of curiosity I looked at the storyboard source for the two scenes and noticed that the scene that wasn't working was much longer than the new one. Looking at the storyboard outline it was obvious why, and surprising it worked at all. And, now I remember having trouble cutting & pasting a prototype cell. Guess I should look at the outline mode more often. See the screen shot below (the visual scenes look the same):
I also had this problem, when I replaced a UITableViewController class with a UIViewController class in code, and didn't update the storyboard i.e. I didn't replace the UITableViewController placeholder in storyboard, with a UIViewController storyboard.
Swift 4.2
You have to tell the tableView what cell is selected.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else { return UITableViewCell() }
let data = products[indexPath.section][indexPath.row]
cell.update(data: data)
// Don't use cell.isSelected = true
if data == selectedData {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
return cell
}
In my case it was the UITableViewController had Clear on Appearance selected at the Storyboard as shown here