UITableViewCell losses repeated content in other cell - objective-c

I've a UITableView which when a cell has the same content that other, this content only appear in the las cell drawed. My custom cell adds an UIView property to add dynamic subviews from other class.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"cell";
CollectionCell *cell = (CollectionCell *)[tableView
dequeueReusableCellWithIdentifier:MyIdentifier];
if (!cell) {
cell = [[[CollectionCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:MyIdentifier]
autorelease];
}
[cell setCollectionView:/* Generated view in other class */];
return cell;
}
The concrete problem is:
My dynamic view is composed by, for example, 2 UILabels:
if label 1 is a title, the title is unique for each row -> No problem, renders fine.
if label 2 is a category, indexes from 0 to 5 have same category -> Only row at index 5 shows category label.
I can't create this labels in cell instantiation and add as subview because the cell content is all dynamic.
Thanks for your time and help.
UPDATE:
I can't create this labels in cell instantiation and add as subview because the cell content is all dynamic.
I'm going to explain it in detail:
The content and UI controls added to collectionView property can be differentes each execution. In one execution collectionView could have an UIImageView and a UILabel, and next execution it has 2 UILabels (for example). This is why I can't create something like this
if (!cell) {
cell = [[[CollectionCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:MyIdentifier]
autorelease];
UILabel *foo = [[UILabel alloc] initWithFrame:SomeFrame];
[foo setTag:101];
[cell.collectionView addSubview:foo];
}
UILabel *foo = [cell.collectionView subviewWithTag:101];
[foo setTitle:#"This content is dynamic"];
Thanks!
Update 2:
Appears to be a problem with custom UILabel subclass. If I use original UILabel to show strings works fine.

You are not supposed to add subviews outside the block-
if (!cell) {
cell = [[[CollectionCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:MyIdentifier]
autorelease];
}
your subviews should only be added inside this block (the first time the reusable cell is created).
everything that happens outside (after) this 'if' block happens multiple times as you scroll your table up and down so that's where you edit the added subviews (only after the whole 'if block, outside it).
See my answer here

Related

How to avoid data being copied to UITableViewCell?

I have an array with charts that I would like to display in a table view. Since drawing the charts takes a few milli seconds, which would make scrolling choppy, I would like to remove the old chart from cell.contentView of a re-used cell and add a new subview with the correct chart when scrolling (see source code below). This works: the charts are correctly displayed.
However, when adding the subviews, the used memory increases and scrolling the first time down is choppy (scrolling is not choppy after all rows were displayed once).
It seems that the chart data, which is already stored in an instance variable (strong) is copied (not just a reference) into the UITableViewCell.
I would like to avoid this so that less memory is used.
Summary: How can I avoid it that my charts are being copied into a UITableViewCell when using addSubview. Instead, I would like to add just a reference to my data when using addSubview.
static NSString *CellIdentifier = #"chart";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell==nil)
{
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
[cell.contentView.subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
VMKChartData *chartData = [shinobiCharts_ objectAtIndex:rowNumber];
ShinobiChart *shinobiChart = chartData.shinobiChart;
[cell.contentView addSubview:shinobiChart];
[self setSeparatorInsets:cell];
return cell;
Don't use a plain UITableViewCell and addSubview:, but create a custom table view subclass that has a property #property (nonatomic, weak) UIView *chartView, in tableView:cellForRowAtIndexPath: just assign it.
cell.chartView = shinobiChart;
in the setter for chartView, add it to the content view.
-(void)setChartView:(UIView *)chartView
{
[_chartView removeFromSuperview];
_chartView = chartView;
[self.contentView addSubView:chartView];
}

Updating subviews in cells on a UITableView

I'm developing an application in iPad 6.0 using Storyboards.
Let me first explain my goal. I'm trying to achieve a Master-Detail (SplitViewController-like) View Controller using 2 UITableViewControllers.
The first UITableView("Master"), let's call this HeaderTableView, as the name implies, lists down the Headers for the...
...Second UITableView("Detail"), let's call this the EncodingTableView, which contains a programmatically changing CustomTableViewCell (subviews contained within each cell may be a UITextField, UIButton or UISwitch).
See EncodingTableView.m
- (void)updateEncodingFields:(NSArray *)uiViewList
{
// Add logic for determining the kind of UIView to display in self.tableView
// Finally, notify that a change in data has been made (not working)
[self.tableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *encodingFieldsTableId = #"encodingFieldsTableId";
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:encodingFieldsTableId];
if (cell == nil) {
cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:encodingFieldsTableId];
}
// Change text in textView property of CustomTableViewCell
cell.encodingFieldTitle.text = uiViewList.title;
// added methods for determining what are to be added to [cell.contentView addSubView:]
// data used here is from the array in updateEncodingFields:
}
My HeaderTableView.m, contains the didSelectRowAtIndexPath to update the EncodingTableView
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (![selectedIndexPath isEqual:indexPath]) {
selectedIndexPath = indexPath;
[self updateDataFieldTableViewForIndexPath:indexPath];
}
}
- (void)updateDataFieldTableViewForIndexPath:(NSIndexPath *)indexPath {
[self.encodingTableView updateEncodingFields:self.uiViewList];
}
Question
- Data is all ok but why doesn't EncodingTableView "redraw"ing the fields? My
suspicion is that reusing cells has something to do with this but I just can't figure out why.
Screenshots on the result:
Initial Selection in HeaderTableView
Second Selection in HeaderTableView
What I've tried :
I kept seeing suggestions such as [UITableView setNeedsDisplay],
[UITableView reloadData] and [UITableView setNeedsLayout] but none of
them worked.
Removing the reuse of tableViewCells works fine but this causes parts of my
CustomTableView.encodingFieldTitle to disappear. Not to mention that this might cause performance issues if I were to drop reusing cells.
Restrictions:
I know that a good idea is to use a SplitViewController but this is just a subpart of my app (hence not the RootViewController).
Finally, thanks for reading such a long post. ;)
It looks like you are most likely adding subviews inside tableView:cellForRowAtIndexPath:.
The issue is that if you use cell reuse then are not always starting from a blank slate inside tableView:cellForRowAtIndexPath: instead you can possibly be given a cell back that has already been configured once. This is what you are seeing, a cell that has previously had labels added to it is handed back to you and then you add some more labels over the top.
There are a few way to deal with this:
(My preferred option) Create a subview of UITableViewCell with these extra sub views available as properties.
Ensure the cell setup is only done once
A great place to do this is when you actually create a cell when one does not already exist e.g. inside the if (cell) check
if (cell == nil) {
cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:encodingFieldsTableId];
// add subview's here and give them some way to be referenced later
// one way of doing it is with the tag property (YUK)
UILabel *subView = [[UILabel alloc] initWithframe:someFrame];
subView.tag = 1;
[cell.contentView addSubview:subView];
}
UILabel *label = (id)[cell.contentView viewWithTag:1];
label.text = #"some value";
One problem i can see in your code is that the cell identifiers used are different in tableView cellForRowAtIndxPath function.
While dequeueing you are using this identifier - > "encodingFieldsTableId"
&
while creating a cell you are using this identifier - > "dataFieldUiGroupTableId".
Ideally these two identifiers should be same !!!
Try adding,
cell.encodingFieldTitle.text = nil;
Before if(cell == nil)
So that whenever your cellForRowAtIndexPath method is called, the string already present in the cell you are going to reuse will get deleted and the new text in uiViewList.title will be displayed.

Why does dynamically added content to a registered and dequeued UITableViewCell persist from one use of the cell to another?

First the UITableViewCell is registered for reuse like this
UINib *cellLoader=[UINib nibWithNibName:#"GroupCell_iPhone" bundle:nil];
[self.tableView registerNib:cellLoader forCellReuseIdentifier:#"GroupCell"];
then in the cellForRowAtIndexPath delegate
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
the cell is dequeued
GroupCell_iPhone *cell=(GroupCell_iPhone*)[self.tableView dequeueReusableCellWithIdentifier:#"GroupCell"];
then a series of UILabel and other objects are created dynamically based upon a series of criteria and added to the cell like this
[cell.contentView addSubview:noActivityLabel];
The problem arises when the second and subsequent cells are dequeued and appear to have the dynamically added objects from the first dequeued cell. In the end, every cell will be different. Is the dequeued cell a "pointer" to one instance of the UITableViewCell? Why do these subsequently dequeued cells have the content from the first?
If this is the case what is the best approach to creating cells w/ dynamic/changing content? Should a new instance of the cell be created each time? Can the dequeued cell be "cloned"?
Clarification:
All cells in the table start w/ a base layout but then unique content is added to each cell based upon some data associated w/ that cell. Therefore, every cell in the table is unique, i.e., there are different subviews (UILable, UIImageView, etc.) in the cells.
I'm not sure if it is the best way but I have done the following:
Created a CustomCellClass with several subclasses: CustomCellType1, CustomCellType2 Custom CellType3 etc. Where the CustomCellSubclasses all use different parts of the Same Model to Display information.
Then, I can add give CustomCellClass a prePareDataForCell function that sets some basic attributes. If the CustomCellSubclass needs them it has them, if not things move along just the same (I think, this is the part I am not sure if it is good practice or not).
Then I can do some pseudocode like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCellClass *returnCell = (CustomCellClass*)[tableView cellForRowAtIndexPath:indexPath];
MyObject *obj = [self.fetchedResultsController objectAtIndexPath:indexPath];
if ([returnCell.reuseIdentifier isEqualToString:CustomCellType1) {
CustomCellType1 *cell = (CustomCellType1*)[tableView dequeueReusableCellWithIdentifier:CustomCellType1];
if (!cell) {
cell = [[CustomCellType1 alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CustomCellType1];
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCellType1" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
// This is a basic cell doesn't need anything special done with the object model
returnCell = cell;
} else if ([[returnCell reuseIdentifier] isEqualToString:CustomCellType2]) {
CustomCellType2 *cell = (CustomCellType2 *)[tableView dequeueReusableCellWithIdentifier:CustomCellType2];
if (!cell) {
cell = [[CustomCellType2 alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CustomCellType2];
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCellType2" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
// Do stuff with obj data only needed for this type of cell, perhaps modify the cells subviews
[cell preConfigureCustomCellType2:obj];
returnCell = cell;
} else {
CustomCellType3 *cell = (CustomCellType3*)[tableView dequeueReusableCellWithIdentifier:StandardMailCell];
if (!cell) {
cell = [[CustomCellType3 alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:StandardMailCell];
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCellType3" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
// Do stuff with obj data only needed for this type of cell perhaps modify the same subviews in a different way for this type of cell
[cell preConfigureCustomCellType3:mail];
returnCell = cell;
}
if (![tableView isDragging] && !tableView.decelerating) {
// I used this to only run an expensive operation after the the customer flicks and the table comes to a rest.
}
return returnCell;
}
Why do these subsequently dequeued cells have the content from the
first?
Instead of re-creating each time new cells, it his far better to re-use your already created cells who are not longer displayed. This is what actually do your tableView when dequeueing your cells: it give you some non-displayed cells if it can.
Say you need to display a tableview with 1000 cells, and the tableView is actually showing only 6 cells at a time. Instead of creating 1000 cells, it will re-use the cells stacks so that it doesn't need to re-create new one.
Is the dequeued cell a "pointer" to one instance of the
UITableViewCell? Why do these subsequently dequeued cells have the
content from the first?
Because it's the same pointer object, so it will have the same content as it was at the first display. You need then to update its data.
Should a new instance of the cell be created each time? Can the
dequeued cell be "cloned"?
You only need to create new cell, when
[self.tableView dequeueReusableCellWithIdentifier:#"GroupCell"]
return nothing. Here's a simple example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// get your cell from the stack
GroupCell_iPhone *cell=(GroupCell_iPhone*)[self.tableView dequeueReusableCellWithIdentifier:#"GroupCell"];
// or create new one if nil. Note: you must give it the same identifier than the
// one you are using for the dequeue. Or tableview will not be able to return you anything from the dequeing.
if(!cell)
cell = [GroupCell_iPhone newWithIdentifier: #"GroupCell"] + autorelease];
** update**
// here you are sure to have a valid cell, so you can display content according to the indexPath
// display: use a controller who will update your cell change
// note, the first method clean what was inside your cell. You can keep the subview and reuse them the same way as tableview do.
[myDisplayClassCellController emptyCellSubview: cell];
// here you add all the display logic you want into your cell view. Note, you need indexPath
// because you need to display depending of the stack.
[myDisplayClassCellController decorateCellWithCustomViews: cell atIndexPath: indexPath];
** end update**
// then you can return back the cell to the tableView
return cell;
}

Custom UITableViewCell Doesn't Draw Corectly

I'm having a really weird issue with my custom UITableViewCell. I have a cell with an identifier of "ThreadCell" in Interface Builder with some custom labels. These labels are tagged so I can access them.
In my code, I am doing the following:
static NSString *CellIdentifier = #"ThreadCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: #"ThreadCell"];
}
Person *person = [self.people objectAtIndex: indexPath.row];
UILabel *nameLabel = (UILabel *)[cell viewWithTag: 0];
nameLabel.text = person.nickname;
return cell;
This seems to work fine, with one exception. The TableView draws like this:
This obviously isn't my custom cell. But the weird thing is, when I tap on a cell, I get this:
This is my custom cell, drawn behind the default cell. What?! I'm not sure how this is happening because I do not ever set the title of the textview anywhere, so I'm not sure where the first John Smith comes from.
Anyone have any ideas?
In your code, you allocate a plain UITableViewCell and not an instance of your custom cell. Setting a reuseIdentifier in initWithStyle is not sufficient to load an instance of a custom cell class.
If you develop for iOS 5 and later, then you can use registerNib:forCellReuseIdentifier: to register a NIB file containing your custom cell. dequeueReusableCellWithIdentifier: will then always return an instance of that NIB.

Modal view controller is slow to appear

I'm having a hard time debugging an issue when presenting a modal view controller. I'm seeing a pause of between 0.5 seconds and 1 second between viewWillAppear being called and viewDidAppear being called on the presented (table view) controller. I tried replacing this with a bare bones table view controller to see if the issue was in the controller calling presentModalController, and it appeared quickly as expected.
I've peppered both controllers with NSLog statements in an attempt to diagnose the issue, but can't narrow it down further than a delay between viewWillAppear and viewDidAppear.
Short of rewriting the controller line by line, what's the best way for me to find out where the issue is? Are there any usual suspects here I should be aware of?
Edit : update with problematic code
The table view is displaying 2 cells, each containing a text field.
I have UITextField properties for each of the 2 text fields
#property (nonatomic, retain) IBOutlet UITextField *itemTextField;
and assign text fields to these properties as follows :
- (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] autorelease];
if (indexPath.row == 0) {
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(110, 10, 185, 30)];
textField.delegate = self;
cell.textLabel.text = #"Item";
textField.placeholder = #"Enter item name";
textField.keyboardType = UIKeyboardTypeDefault;
textField.returnKeyType = UIReturnKeyNext;
self.itemTextField = textField;
[cell addSubview:textField];
[textField release];
}
}
return cell;
}
I've left out the second row, but the code is the same.
If I comment out
self.itemTextField = textField;
the view loads as expected, but uncommented causes the slight delay I've been seeing. Should I be initialising this somewhere else rather than in cellForRowAtIndexPath? I'm a bit stumped.
Use Time Profiler in Instruments to see which is the offending code. Also note that excessive logging itself can cause noticeable speed degradation. Likely cases are costly methods to provide data to your table view, custom heights perhaps? Or loading of content from a network synchronously.
Got the same problem. Just fixed it with barrym's comment. Just move the becaomeFirstResponder code to viewDidAppear