I have a UITableView with about 15 items. I wanted to long press on a cell to view the copy menu item to copy the cell's content and insert it right below the cell where it was copied from. Here's my code;
- (void)tableView:(UITableView*)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath*)indexPath withSender:(id)sender
{
// Insert the copied item below the item where it was copied from
if (action == #selector(copy:)) {
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
self.copiedColor = cell.textLabel.text;
[self.colors addObject:self.copiedColor];
NSIndexPath *path = [NSIndexPath indexPathForRow:indexPath.row inSection:0];
[self.tableView beginUpdates];
[tableView insertRowsAtIndexPaths:#[path] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
}
- (BOOL)tableView:(UITableView*)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath*)indexPath withSender:(id)sender
{
// Show the copy menu item for cells
if (action == #selector(copy:)) {
return YES;
}
return NO;
}
- (BOOL)tableView:(UITableView*)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath*)indexPath
{
return YES;
}
The copy menu appears and I can copy the content with no problem. The problem is in the inserting part. It actually inserts the cell but the content is sometimes messed up. For example here I'm copying the color Crimson. It gets copied and the new cell appears under the original one. But notice that another cell with Crimson appears as the last row!
And if I copy the color Beige, instead of it getting copying, Aqua gets copied but doesn't appear the it either. It's a other whole mess!
Can anyone please tell me how to correct this?
If it's too difficult to understand it by the code and images I've posted, I uploaded a test runnable xcode project here to demonstrate the issue. Please have a look.
Thanks a ton in advance.
I think this line:
[self.colors addObject:self.copiedColor];
Should be:
[self.colors insertObject: self.copiedColor atIndex: [indexPath row]];
Otherwise you keep adding objects at the end of your color array, but in the table view you insert them at a different index so the data source and your table view become out of sync in a way.
Also this line:
[tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
You can change to simply:
[tableView dequeueReusableCellWithIdentifier:#"cell"];
For the other strange behavior you are getting. In addition, checking if cell is nil in your case is not necessary since you have declared a prototype of the cell in your storyboard and the method will not return nil.
Related
I have a multi-selection UITableView created with prototype cells from a storyboard. I am able to make multiple selections,
I am finding that if I select the cell in row 0, I am unable to select any cell a multiple of 9 rows away. i.e. 9, 18, 27, etc.
If I deselect 0, I am then able to select row 9. Doing this, however, makes rows 0, 18, 27, etc unselectable.
I have implemented the following delegate method for logging, however, when clicking these unresponsive cells, this method does not get called.
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if([[tableView cellForRowAtIndexPath:indexPath] isSelected]) {
NSLog(#"Already Selected");
} else {
NSLog(#"New Select");
}
return indexPath;
}
I have tested this on an iOS8.3 iPhone 6 Plus, an iOS9.2 iPhone 6S and an iOS9.2 iPhone 5S with the same results and the same distancing.
I don't believe it is a coincidence that there are 9 cells that fit on the screen at any one time. I assume it is connected with cell dequeuing and re-use but I've not been able to confirm that.
Does anyone have any direction on what it is that I am missing?
EDIT: Included tableView:cellForRowAtIndexPath
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
DKIFriendTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier forIndexPath:indexPath];
NSArray *array;
switch ([indexPath section]) {
case 0:
array = [recommendedFilteredList objectAtIndex:[indexPath row]];
break;
case 1:
array = [fullFilteredList objectAtIndex:[indexPath row]];
break;
default:
return nil;
break;
}
BOOL selected = [selectedUsers containsObject:[array objectAtIndex:0]];
// Due to conflicts with searching table, set whether cell is selected or not.
if(selected)
[tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
else
[tableView deselectRowAtIndexPath:indexPath animated:NO];
[cell setNameText:[array objectAtIndex:1]];
[cell setProfilePictureImage:[UIImage imageWithData:[[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:[array objectAtIndex:2]]options:NSDataReadingMappedIfSafe error:nil]]];
return cell;
}
It is probably worth mentioning that the problem still occurs whether the selected boolean is checked and implemented or not. Just if it is not implemented, the search results cells do not come back as highlighted.
EDIT: Included numberOfSectionsInTableView: delegate method for further information relevant to the tableView:cellForRowAtIndexPath method.
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
EDIT: Addition of sample project BitBucket repository link
After disposing of the code I previously had and attempting to recreate the views from scratch, I still experienced the same problems. I then created only this section of the project again in a new, separate project where I still experienced the same problem (this time over a different separation - n * 12 instead of n * 9).
Sample Project BitBucket link
i checked a project and find the problem.
- (void) setSelected:(BOOL)selected {
[_tickImage setHidden:!selected];
}
you override a method of tableviewcell however not called the super function. after doing this, all the flow in selection/deselection is acted like you wanted.
- (void) setSelected:(BOOL)selected {
[super setSelected:selected];
[_tickImage setHidden:!selected];
}
Your mistake is thinking that [[tableView cellForRowAtIndexPath:indexPath] isSelected] is a valid way to see if a row is selected.
If there's no visible cell for the row at indexPath, then [tableView cellForRowAtIndexPath:indexPath] returns nil. In Objective-C, you can send any message to nil, and get back nil/false/0. So if the row at indexPath has no visible cell, your test will always return false, saying the row isn't selected.
The correct way to check whether the table view thinks a row is selected is to ask the table view which rows are selected:
if ([tableView.indexPathsForSelectedRows containsObject:indexPath]) {
// row is selected
}
Let's start right off with some code :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"forIndexPath:indexPath];
Produit *object = self.objects[indexPath.row];
[cell.contentView addSubview:object];
return cell;
}
In a cell, I add a subView of type Produit, which is a subclass of UIView. This is how it looks like:
Editing all the stuff works fine except for when there are more cells than the size of the screen can allow. When that is the case, if I try and modify some info in one of the cells, it's as if the new info is added on top of the old one like this:
In this image, only the Button acts spooky but sometimes the text fields also appear on top of each other. What's more is that if I modify the cell on top, then if I scroll to the bottom of the table view, the last cell also gets modified. Last thing: when I add more cells after having produced this glitch, some of the new cells get the same 'Category' as the glitched one, it's like it's making a copy of it and puts in 'Category' the glitched title...
Can someone explain what's happening? How can I fix it? Here is some more code( not all of it, just the table view configuration)
-(void) addNewProduit:(UIBarButtonItem*) item {
if (!self.objects) {
self.objects = [[NSMutableArray alloc] init];
}
Produit* product = [[Produit alloc] init];
CGRect frame = CGRectMake(0, 0, self.frame.size.width, 44);
[product setFrame:frame];
[product initView];
[self.objects insertObject:product atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.objects removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[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.
}
}
dequeueReusableCellWithIdentifier: forIndexPath: gives you a cell, which might be a new cell, or it might be an old cell that's previously been shown, but has scrolled off the screen.
One quick fix is:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"forIndexPath:indexPath];
Produit *object = self.objects[indexPath.row];
[cell.contentView.subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
[cell.contentView addSubview:object];
return cell;
This will resolve your issue in the least efficient way possible. It is likely to cause jittery animation when you scroll really fast, especially on older devices. I just intend it as an illustration of the problem you need to solve.
A more appropriate solution would reuse the view if it's already there, instead of creating a new one each time.
self.objects appears to contain views, which defeats the purpose of UITableView's really fast scrolling setup. You should just include data objects there, and then configure the views for an individual cell when it's time to show that one cell. IE, you don't want a view for each data object, you want 6 views that adapt to which data object currently needs to be displayed.
You are always adding more views when you re-use a cell by [cell.contentView addSubview:object];. One solution might be to tag the view when you add it and then remove any subview with the appropriate tag before adding another one.
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 the users taps on a cell, I want to update my UITableView; including the contents of this tapped cell. Easiest way is to update internal parameters and then invoke [self.tableView reloadData];.
However, reloadData immediately stops the nice blue->none selection animation of my tapped cell.
Is there a (standard) way to update my table cells without stopping the tapped cell's animation?
Note in this case I don't add or delete cells; I just want to contents to change (e.g. start an activity indicator, or change the color of labels.)
In your case you can just get pointers to all visible cells and update them. Something like this:
- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
NSArray* visibleCells = [tableView indexPathsForVisibleRows];
for (NSIndexPath* indexPath in visibleCells)
{
UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self updateCell:cell atIndexPath:indexPath]; // Your method, which updates content...
}
}
And if you want to update other cells content you can use something like this:
- (void)tableView:(UITableView*)tableView willDisplayCell:(UITableViewCell*)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
[self updateCell:cell atIndexPath:indexPath]; // Your method, which updates content...
}
So your cells will always display correct content.
About creating content:
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* CellIdentifier = #"Cell";
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
[self createContentForCell:cell atIndexPath:indexPath]; // so here to create content or customize cell
}
return cell;
}
Or maybe you could simply delay the reload of your table data as described for example here: Delay reloadData on UITableView
I need to detect when a tableview has finished reloading data. There was an older solution where you could subclass the tableview then overload the reloadData method, however apparently that no longer works because tables are handled on multiple threads now and reloadData is called before cellForRowAtIndexPath.
My question is, has there been any solution to this problem since the change?
My problem is I am losing the pointer to a textField when the table reloads its data, so the first responder I am trying to set to the next text field (to auto focus on the next data input field), is lost.
This is essentially a repeat of #wain 's answer, but I thought I would add a little code.
You can keep a reference to the index path of the cell that owns the active text field (as a property).
Then, in cellForRowAtIndexPath: something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
MyTableViewCell *cell = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//I would hold a reference to the text field as a property on a subclass of UITableViewCell so that you can check for whether it exists.
if (!cell.textField) {
cell.textField = [[UITextField alloc] initWithFrame:cell.contentView.frame];
[cell.contentView addSubview:cell.textField];
}
return cell;
}
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
UITableViewCell *cell = (UITableViewCell *)textField.superview.superview;
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
self.indexPathForActiveTextField = indexPath;
}
- (BOOL) textFieldShouldReturn:(UITextField *)textField
{
MyTableViewCell *cell = (MyTableViewCell *)textField.superview.superview;
NSIndexPath *ip = [self.tableView indexPathForCell:cell];
[self.tableView reloadData];
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:ip.row+1 inSection:ip.section];
MyTableViewCell *theNewCell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:nextIndexPath];
if (theNewCell) {
[theNewCell.textField becomeFirstResponder];
}
return YES;
}
Store the NSIndexPath of the table cell that holds the text field that should be the first responder. When you want to change the first responder you can ask the table view for the cell at that index path, then find the text field and make it first responder.
If the table view gets reloaded, in cellForRowAtIndexPath: check the index path and make the 'new' text field the first responder.
In this way you can set the first responder at any time and you can't loose the reference to it as the reference is to a location, not an object (which will be reused or removed).
UITableView uses a pool to reuse displayed cell. The target cell is possibly reused in other row. Storing the NSIndexPath like Wain suggested is good untill you not reorder the cells or delete some entry from the datasource. Define a key in the model, that you set the firstResponder according to that. Hope i did not misunderstood the problem.