How to avoid data being copied to UITableViewCell? - objective-c

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];
}

Related

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.

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.

Activity indicator only shows in one cell

I have a UITableView filled with cells that display file names, and the cells also indicate the upload/download progress of the files. When a file is being moved, its corresponding cell should show a UIActivityIndicatorView in its accessory view. I have a UIActivityIndicatorView set up and ready to go in viewDidLoad, but when I tried to set it as the accessory view for multiple cells, it only shows up in one cell.
-(void)viewDidLoad {
[super viewDidLoad];
activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[activityIndicator startAnimating];
}
//...code that detects file changes and calls fileChange
-(void)fileChange {
for (int i = 0; i < [self.cloudNames count]; i++) {
//detect whether file name in array is uploading, downloading, or doing nothing
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (downloading) {
cell.accessoryView = activityIndicator;
//other changes here specific to downloads
} else if (uploading) {
cell.accessoryView = activityIndicator;
//other changes here specific to uploads
} else {
cell.accessoryView = nil;
}
}
}
As I said, the activity indicator only shows in one cell even where there are multiple cells that should show it.
I don't want to set up the UIActivityIndicatorView in the fileChange method (even though it works) because this method is called many times during the upload/download. If the method is called and the activity indicator is set up there, the activity indicator resets in all of the table view cells when the method is called, resulting in glitchy and unsmooth animations, and it causes a huge memory problem.
Any ideas of what to do? Thanks.
Even if you are setting the activity indicator for the cell, you only ONE instance variable for it. The way to do this is to create an indicator for each cell inside tableView:cellForRowAtIndexPath:
You can set a tag for the UIActivityIndicatorView and whenever you want to access it or grab it you can get the cell, and get the indicator view using [cellView viewWithTag:theTag]. No need for instance variables.
If you want to make things even fancier you can subclass UITableViewCell and do whatever you want to do inside your custom cell..
EDIT:
To get the view you can either assign to the accessory view and just get the cells accessoryView:
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIActivityIndicatorView *indicator = (UIActivityIndicatorView *) cell.accessoryView;
or you can add the UIActivityIndicatorView to the cell`s contentView (that way you can place it where ever you want, you have more flexibility):
adding the indicator:
myIndicatorView.tag = 1;
[cell.contentView addSubview:myIndicatorView];
getting the indicator:
UIActivityIndicatorView *indicator = [cell.contentView viewWithTag:1];
hope this helps

UITableViewCell losses repeated content in other cell

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

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