UITableViewCell spooky action - ios

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.

Related

UITableView reuse Questions

About UITableView reuse, when there are multiple different Cell, use a different identifier to distinguish good or use an identifier and the Cell subViews remove, add content again good, if the Cell is very many cases, these reusable, what kind of specific access rules, when an identifier in the queue on the position is how to remove the master answer, thank you
_testTableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_testTableView.dataSource = self;
_testTableView.delegate = self;
[_testTableView setRowHeight:80.];
[self.view addSubview:_testTableView];
[_testTableView registerClass:[TestTableViewCell class] forCellReuseIdentifier:testKeyOne];
[_testTableView registerClass:[TestTwoTableViewCell class] forCellReuseIdentifier:testKeyTwo];
//one way
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//one
if(indexPath.row < 15){
TestTableViewCell * oneCell = [tableView dequeueReusableCellWithIdentifier:testKeyOne forIndexPath:indexPath];
return oneCell;
}else{
TestTwoTableViewCell * oneCell = [tableView dequeueReusableCellWithIdentifier:testKeyTwo forIndexPath:indexPath];
return oneCell;
}
return nil;
}
two way:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:testKeyOne forIndexPath:indexPath];
for(UIView * view in cell.subviews){
[view removeFromSuperview];
}
//[cell addSubview:];
return cell;
}
one way or two,or Other better way,and Reuse of specific originally, enter the reuse and take out the order of the queue order
You want to go with option one. The table view data source should not be adding or removing views from table view cells. That just gets too messy.
Another option is to have just one cell subclass, but code the subclass to hide and show views as needed. I wouldn't have it add and remove views. That's way more complex code and way more expensive time-wise, which isn't great when you're trying to get a high frame rate when scrolling.

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.

Toggle between TableViewCells, setting accessoryView and Data

i am building a social App, where you can send messages - its not a chatting app, only sending objects ( i am learning to code). I want to select only one friend from my tableView and send the message to him. I also want to add my cell.accessoryView only on one cell, the selected one. When i hit another row, the last cells accessoryView gets deselected. Also disable multiple touches on one row = if i select one friend, the only way how to deselect him, is to select another cell. I also created a string and i want to add only the cell.textLabel.text from the selected cell, if i select another cell, old info gets deleted and new info added. I tried a lot of stuff, but nothing works and i didn't find any tutorial or answer :(. Does anybody have an idea, how to do this? Thank you very much for answers:
This is the lat code i tried, it works but if i toggle more than 2 times it gets corrupted
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
//checkmark is my image for custom checkmark
if (cell.accessoryView == checkmark)
{
cell.accessoryView = nil;
}
else
{
cell.accessoryView = checkmark;
}
}
add this method in your tableview class
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell * currentCell = [tableView cellForRowAtIndexPath:indexPath];
UITableViewCell * oldCell = [tableView cellForRowAtIndexPath: oldIndexPath];
oldCell.accessoryView = nil;
currentCell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkMark"]];
oldIndexPath = indexPath; // instance Variable NSIndexPath * oldIndexPath;
}

Add new UITableView row with custom text

Using this code
- (IBAction)testAdd:(id)sender
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows inSection:0];
[self.tableView beginUpdates];
self.numberOfRows++;
[self.tableView insertRowsAtIndexPaths:#[indexPath]withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
I'm able to add a new item to a tableView via an 'add' button on the app. This basically adds an item identical to the item already on the table that preceded it.
For example, I have a tableview with the first row displaying a string "TEST", hitting add adds another row that displays "TEST".
I would like to be able to pass in a custom value for the new row, so hitting add outputs a row with say "NEWTHING".
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell" forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = self.val2;
return cell;
}
My data source is actually another view controller that takes user inputs and sends it to my tabelViewController, with the text for the item as "val2".
What I actually want to achieve is the ability to hit add, go back to the user input view controller, get the new data and send it back to my tableViewController to be displayed
What you're asking, is the kinda stuff that is to be done in -cellForRowAtIndexPath: (most of the times, it depends on the way you have designed your datasource) but if it doesn't matter to you, then you can do:
- (IBAction)testAdd:(id)sender
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows
inSection:0];
self.numberOfRows++;
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell.textLabel setText:#"NEWTHING"];
}
But note that when you scroll far up/down and return to this cell, it will most probably show "TEST" (that's where -cellForRowAtIndexPath: will show it's true purpose)
PS: Include your -cellForRowAtIndexPath: method implementation in the question if you want to proceed further
EDIT:
Your -cellForRowAtIndexPath is too static... in the sense that it simply sets self.val2 to cell.textLabel.
Lets say you start with 10 rows, -cellForRowAtIndexPath will be called 10 times and every time, it will set self.val2 onto the current cell's textLabel.
Now... when you add one row (on a button tap), the -cellForRowAtIndexPath will be called for the 11th cell and the same* text will be set to it.
*this technically happened but we quickly changed the cell's text
Basically, the tableView doesn't know how to differentiate between an existing cell and a new added cell because the datasource itself is not dynamic.
To direct the tableView on how to handle different cells, we need to create a more dynamic datasource.
There are different approaches use but I'd generally do it this way:
- (void)viewDidLoad
{
[super viewDidLoad];
self.val2 = #"TEST";
//declare "NSMutableArray *arrDatasource;" globally
//this will be the soul of the tableView
arrDatasource = [[NSMutableArray alloc] init];
int i_numberOfCells = 10;
//populate beginning cells with default text
for (int i = 0; i < i_numberOfCells; i++) {
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:self.val2 forKey:#"displayText"];
[arrDatasource addObject:dictionary];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//return number of objects in arrDatasource
return arrDatasource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell" forIndexPath:indexPath];
//pick up value for key "displayText" and set it onto the cell's label
[cell.textLabel setText:arrDatasource[indexPath.row][#"displayText"]];
//this will be dynamic in nature because you can modify the contents
//of arrDatasource and simply tell tableView to update appropriately
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//make indexPath of new cell to be created
NSIndexPath *indexPathNEXT = [NSIndexPath indexPathForItem:arrDatasource.count inSection:0];
//add the appropriate contents to a dictionary
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:#"NEWTHING" forKey:#"displayText"];
//add the dictionary object to the main array which is the datasource
[arrDatasource addObject:dictionary];
//add it to tableView
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPathNEXT]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
//this ends up calling -cellForRowAtIndexPath for the newly created cell
//-cellForRowAtIndexPath shows the text (you put in the dictionary in this method above)
}
PS: -cellForRowAtIndexPath: is called whenever cell updates or refreshes or needs to be displayed and so this method needs to be implemented properly

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.

Resources