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
}
Related
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 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.
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.
My table is relatively simple and not radically different from many I've done before. Yet didSelectRowAtIndexPath is only called on the first 5 cells of the table. After that, the debug statement does not appear when I tap. I've researched this issue here and have ruled out some possibilities that are mentioned in other questions:
- the table delegates are properly set.
- a GestureRecognizer (that I've set) is not swallowing the presses.
- willSelectRowAtIndexPath is not implemented
Below is my didSelectRowAtIndexPath. Let me know what else I can provide that can help solve this problem.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"didSelect");
NSArray *visibleCells = [partOfSpeechTable visibleCells];
UITableViewCell *cell = [visibleCells objectAtIndex:indexPath.row];
NSNumber *checkedState = [checkedStates objectAtIndex:indexPath.row];
if ([checkedState boolValue])
{
[cell setAccessoryType:UITableViewCellAccessoryNone];
[checkedStates setObject:[NSNumber numberWithBool:NO] atIndexedSubscript:indexPath.row];
}
else
{
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
[checkedStates setObject:[NSNumber numberWithBool:YES] atIndexedSubscript:indexPath.row];
}
[[tableView cellForRowAtIndexPath:indexPath] setSelectionStyle:UITableViewCellSelectionStyleNone];
}
(I can comment out all the accessory stuff and it makes no difference).
Thanks for any help.
It turned out that the containing view was shorter than the table itself. The table displayed in full, but the parts below the cut off of the containing view did not respond to user interaction. The solution was to increase the size of the containing view.
I have faced similar kind of issue its probably because of
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 20;
}
set the size of that cell so it can fit in view.
I had the same problem with Jason. In my case, only the cell on the first row wasn't calling didSelectRowAtIndexPath. I increased the containing view of the tableview in xib file (or you could do programmatically).
If didSelectRowAtIndexPath is called only for some cells and the app is running in the simulator then try to reset the simulator.
Menu Hardware > Erase All Content and Settings...
I´m quite new to iOS development and I´m having a terrible time by trying something that should be easy; to add an extra row in a TableView everytime the user clicks on one of the existing rows. There is no real purpose on that action, I´m just wanting to understand the behaviour of TableView.
So I did the following:
I used a Split View-based template and changed the number of rows to 30 in the RootViewController.
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return 30;
}
The method tableView:didSelectRowAtIndexPath looks in the following manner:
- (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.
*/
NSMutableArray* paths = [[NSMutableArray alloc] init];
NSIndexPath *indice = [NSIndexPath indexPathForRow:30 inSection:0];
[paths addObject:indice];
detailViewController.detailItem = [NSString stringWithFormat:#"Second Story Element %d with all its information and bla bla bla", indexPath.row];
[[self tableView] beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[[self tableView] endUpdates];
}
When I execute the program and click on one of the elements, I receive the following 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 (30) must be equal to the number of rows contained in that section before the update (30), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted).'
I did not change any other part of the code that the template provides.
I read quite extensively the documentation from Apple and the responses to the following questions:
Add a row dynamically in TableView of iphone
and
how to properly use insertRowsAtIndexPaths?
The second question seems to address the same problem, but I´m not capable to understand what is happening. What do they mean with dataSource? The response that I understand better says the following:
It's a two step process:
First update your data source so numberOfRowsInSection and cellForRowAtIndexPath will return the correct values for your post-insert data. You must do this before you insert or delete rows or you will see the "invalid number of rows" error that you're getting.
What does this update of the data source implies?
Sample code would be HIGHLY appreciated, because I´m totally frustrated.
By the way, all that I´m trying has nothing to do with entering the editing mode, has it?
You need to keep the count returned by tableView:numberOfRowsInSection: in sync!
So when you have 30 rows and then tell the tableview to insert a new row you need to make sure tableView:numberOfRowsInSection: will now return 31.
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section
{
return self.rowCount;
}
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
self.rowCount++;
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
In practice you would probably use an array to track your rows return [self.rows count]; etc
The answer is quite simple. When you want to modify a table view you need to perform two simple steps:
Deal with the model
Deal with table animation
You already perform the second step. But you have missed the first one. Usually when you deal with a table you pass it a data source. In other words some data to display within it.
A simple example is using a NSMutableArray (it's dynamic as the name suggests) that contains dummy data.
For example, create a property like the following in .h
#property (nonatomic, strong) NSMutableArray* myDataSource;
and in .m synthesize it as:
#synthesize myDataSource;
Now, you can alloc-init that array and populate it as the following (for example in viewDidLoad method of your controller).
self.myDataSource = [[NSMutableArray alloc] init];
[self.myDataSource addObject:#"First"];
[self.myDataSource addObject:#"Second"];
Then, instead of hardcoding the number of rows you will display (30 in your case), you can do the following:
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
return [self.myDataSource count];
}
Now, in you didSelectRowAtIndexPath delegate you can add a third element.
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.myDataSource addObject:#"Third"];
[[self tableView] beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[[self tableView] endUpdates];
}
It looks like one big problem is with tableView:numberOfRowsInSection:. You need to return the correct number of rows in that method.
To do that, it's usually best to maintain an NSArray or NSMutableArray of items for the table view so in that function, you can say: return [arrayOfValues count];. Keep the array as a property of your view controller class so that it's readily accessible in all methods.
The array can also be used in cellForRowAtIndexPath:. If you have an array of NSString, you can say cell.text = [arrayOfValues objectAtRow:indexPath.row];.
Then, when you want to add an item to the table view, you can just add it to the array and reload the table, e.g. [tableView reloadData];.
Try implementing this concept and let me know how it goes.
You can Also do that for dayanamic table cell
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [arrayStationStore count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIndentyfire;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIndentyfire];
if (cell == nil)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIndentyfire];
}
cell.textLabel.text = [arrayStationStore objectAtIndex:indexPath.row];
return cell;
}
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Check if current row is selected
BOOL isSelected = NO;
if([tblStationName cellForRowAtIndexPath:indexPath].accessoryType == UITableViewCellAccessoryCheckmark)
{
isSelected = YES;
}
if(isSelected)
{
[tblStationName cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryNone;
[arrayReplace removeObject:indexPath];
NSLog(#"array replace remove is %# ",arrayReplace);
}
else
{
[tblStationName cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryCheckmark;
[arrayReplace addObject:indexPath];
NSLog(#"array replace add is %# ",arrayReplace);
}
return indexPath;
}