I have a UItableview, which I'm populating with data, using heightForRowAtIndexPath and cellForRowAtIndexPath. Apparently Apple makes me do things in my code twice.
First I have to calculate the size of my views (for that I have to make them) in heightForRowAtIndexPath and then I have to make them again, to add them to the actual view.
I have a pretty complicated view, so it looks double ugly, when you have to write it twice.
Isn't there a better way to do this?
UPDATE
This is how my code looks. It's not totally the same, but pretty close. Why in the world does apple make me write this twice?
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(#"heightForRowAtIndexPath");
//Initiating strings
NSString *headlineString;
NSString *subHeadlineString;
NSString *bylineString;
if (global.magazine.issues.count==0) {
return 45;
}else if(indexPath.section == global.magazine.issues.count+1) {
//Finding the right issue and article for this row
Issue *issue = [global.magazine.issues objectAtIndex:global.magazine.issues.count-1];
//Creating the headline
headlineString = [NSString stringWithFormat:#"<span class='bold_style'>FOREWORD</span>"];
//Creating the subHeadline
subHeadlineString = [NSString stringWithFormat:#"%#", [issue.magazine_foreword substringToIndex:100]];
//Creating byline
bylineString = [[NSString stringWithFormat:#"<span class='ital_style'>By %#</span>", issue.magazine_byline] capitalizedString];
}else{
//Finding the right issue and article for this row
Issue *issue = [global.magazine.issues objectAtIndex:indexPath.section-1];
Article *article = [issue.articles objectAtIndex:indexPath.row];
//Creating the headline
headlineString = [NSString stringWithFormat:#"<span class='bold_style'>%#</span>", [article.title uppercaseString]];
//Creating the subHeadline
subHeadlineString = [NSString stringWithFormat:#"%#", [article.main_text substringToIndex:100]];
//Creating byline
bylineString = [NSString stringWithFormat:#"<span class='ital_style'>By %#</span>", article.byline];
}
//Creating the labels
NMCustomLabel *headline = [global.label headLineLabelWithString:headlineString fromTop:30 withWidth:global.screenWidth-60];
NMCustomLabel *subHeadline = [global.label subHeadlineLabelWithString:subHeadlineString fromTop:30+headline.height+10 withWidth:global.screenWidth-60];
NMCustomLabel *byline = [global.label articleBylineLabelWithString:bylineString fromTop:30+headline.height+10+subHeadline.height+10 withWidth:global.screenWidth-60];
//Setting the height of the row
return 30+headline.height+10+subHeadline.height+10+byline.height+30;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(#"cellForRowAtIndexPath");
//Preparing the cell
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
//Removing former text views
for (UIView *subview in [cell subviews]) {
if (subview.tag == 21 || subview.tag == 22 || subview.tag == 23) [subview removeFromSuperview];
}
//Removing and setting tableview border
[[cell viewWithTag:30] removeFromSuperview];
UIView *rightBorder = [[UIView alloc] initWithFrame:CGRectMake(cell.width-1, 0, 1, cell.height)];
rightBorder.backgroundColor = global.lightGrey;
rightBorder.tag = 30;
[cell addSubview:rightBorder];
//Setting the seletion background color on the cells
UIView *bgColorView = [[UIView alloc] init];
bgColorView.backgroundColor = global.extraLightGrey;
cell.selectedBackgroundView = bgColorView;
if (global.magazine.issues.count==0) {
return cell;
}else if (indexPath.section-1 == global.magazine.issues.count) {
//Finding the right issue and article for this row
Issue *issue = [global.magazine.issues objectAtIndex:global.magazine.issues.count-1];
//Creating the headline
NSString *headlineString = [NSString stringWithFormat:#"<span class='bold_style'>FOREWORD</span>"];
NMCustomLabel *headline = [global.label headLineLabelWithString:headlineString fromTop:30 withWidth:global.screenWidth-60];
headline.tag = 21;
[cell addSubview:headline];
//Creating the subHeadline
NSString *subHeadlineString = [[NSString stringWithFormat:#"%#", issue.magazine_foreword] substringToIndex:100];
NMCustomLabel *subHeadline = [global.label subHeadlineLabelWithString:subHeadlineString fromTop:30+headline.height+10 withWidth:global.screenWidth-60];
subHeadline.tag = 22;
[cell addSubview:subHeadline];
//Creating byline
NSString *bylineString = [[NSString stringWithFormat:#"<span class='ital_style'>By %#</span>", issue.magazine_byline] capitalizedString];
NMCustomLabel *byline = [global.label articleBylineLabelWithString:bylineString fromTop:30+headline.height+10+subHeadline.height+10 withWidth:global.screenWidth-60];
byline.tag = 23;
[cell addSubview:byline];
}else{
//Finding the right issue and article for this row
Issue *issue = [global.magazine.issues objectAtIndex:indexPath.section-1];
Article *article = [issue.articles objectAtIndex:indexPath.row];
//Creating the headline
NSString *headlineString = [NSString stringWithFormat:#"<span class='bold_style'>%#</span>", [article.title uppercaseString]];
NMCustomLabel *headline = [global.label headLineLabelWithString:headlineString fromTop:30 withWidth:global.screenWidth-60];
headline.tag = 21;
[cell addSubview:headline];
//Creating the subHeadline
NSString *subHeadlineString = [NSString stringWithFormat:#"%#", [article.main_text substringToIndex:100]];
NMCustomLabel *subHeadline = [global.label subHeadlineLabelWithString:subHeadlineString fromTop:30+headline.height+10 withWidth:global.screenWidth-60];
subHeadline.tag = 22;
[cell addSubview:subHeadline];
//Creating byline
NSString *bylineString = [[NSString stringWithFormat:#"<span class='ital_style'>By %#</span>", article.byline] capitalizedString];
NMCustomLabel *byline = [global.label articleBylineLabelWithString:bylineString fromTop:30+headline.height+10+subHeadline.height+10 withWidth:global.screenWidth-60];
byline.tag = 23;
[cell addSubview:byline];
}
return cell;
}
The easiest solution is to follow DRY principles and either add height as a property of the objects you are using as a datasource or add a method to your view controller such as:
-(CGFloat)calculateHeightForHeadline:(NSString*)headline andSubHeadline:(NSString*)subHeadline andByLine:(NSString*)byLine
Then at least you only have the calculation code in one place.
Alternatively, you could call [tableView heightForRowAtIndexPath:indexPath] from your cellForRowAtIndexPath method
You have to do it twice because the table view needs to know how tall it is in total before it draws anything - so it calls the height method for every row before it calls any cell method. With your current code, depending on the number of rows, you may be experiencing a slight delay before the table appears - instruments will show you that it is the height method you're spending time in.
I don't know what you custom label classes do but you may be able to calculate the height without having to create views (which is expensive) by using the string or attributed string drawing and size calculation UIKit extensions, which were created for this exact purpose.
heightForRowAtIndexPath
Will be called for all your rows. This delegate method returns the height for your rows. As your design required different height for each rows depending upon the condition. Unfortunately you'll have to calculate the row height each time before your row gets created. It's like we are deciding just before drawing the row what will be it's hight.
There's one way you can avoid this. But I won't advice you to go for it. Because it'll change the design of your UITableView. What you can do is you can decide the maximum height of your row out of all the possible conditions you have. For eg. Let's consider it as 100 pixel. Then you can draw your rest of the cells. However, that'll leave empty space if any of your row is less then 100 pixel. And it'll look shaggy.
Basically, to meet your requirement you'll have to do this twice. No other choice :-(
I wonder, why are you creating your custom labels in heightForRowAtIndexpath? Why don't you just calculate the size of the text with sizeWithFont: or such methods? I think that would be a better way to calculate height of the row. Good Luck!
Related
I have a programmatically generated UITableView with many UILabel's.
Each added UILabel should be seen in front.
All works ok until I add the final UILabel, which appears behind all the others.
How can I bring it to the front?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
if (cell == nil)
{
if( dbg ) NSLog( #" - cell nil");
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: CellIdentifier];
/* Though it's UITableViewCellStyleDefault, the three defaults (image, label, detail label) are nil
if not set. */
// UI controls must be preset for re-used, to prevent memory leak:
// Allocate max. possible UI controls for this row, once per boot:
int instance;
for( instance=0; instance < MAX_CELL_UILABEL; ++instance )
{
UILabel* cell_UILabel = [[UILabel alloc] initWithFrame: CGRectZero]; // allocate next UI control
[cell.contentView addSubview: cell_UILabel ]; // add it permanently to the cell
cell_UILabel.tag = BASE_UILABEL_TAG + instance; // assign unique ID for later lookup
}
...
OTHER UILABELS ARE ADDED HERE.
AND, HERE IS THE FINAL UILABEL, WHICH APPEARS BEHIND THE REST, WHEN IT SHOULD APPEAR IN FRONT:
UILabel* battery_percent = (UILabel*)[cell.contentView viewWithTag: BASE_UILABEL_TAG + ul++];
battery_percent.frame = CGRectMake (x,y, w,h);
battery_percent.backgroundColor = [UIColor orangeColor];
battery_percent.textAlignment = NSTextAlignmentCenter; // NSTextAlignmentRight, NSTextAlignmentLeft
battery_percent.font = [UIFont systemFontOfSize: font_size];
battery_percent.textColor=[UIColor whiteColor];
battery_percent.numberOfLines=0;
// Show battery %:
battery_percent.text = [NSString stringWithFormat: #"%d%%", battery_charge_percent ];
[cell bringSubviewToFront:label];
I found the answer elsewhere on Stackoverflow:
[cell.contentView bringSubviewToFront: battery_percent];
Sweet!
From
int instance;
for( instance=0; instance < MAX_CELL_UILABEL; ++instance )
{
UILabel* cell_UILabel = [[UILabel alloc] initWithFrame: CGRectZero]; // allocate next UI control
[cell.contentView addSubview: cell_UILabel ]; // add it permanently to the cell
cell_UILabel.tag = BASE_UILABEL_TAG + instance; // assign unique ID for later lookup
}
The battery_percent label should have a tag value of (BASE_UILABEL_TAG + MAX_CELL_UILABEL - 1)
When you grab the battery_percent label later on with
UILabel* battery_percent = (UILabel*)[cell.contentView viewWithTag: BASE_UILABEL_TAG + ul++];
What is the value of ul at this point?
If it isn't equivalent to (MAX_CELL_UILABEL - 1) then you're grabbing the wrong label.
Here I am not sure where you are adding the battery_percent label...either cell or cell.contentView
So I would suggest you to Use this.
[[battery_percent superView] bringSubviewToFront:battery_percent];
Hope this will help you.
My UITableView, after the messages (content) is loaded into the cells, experiences a very noticeable lag in scrolling and sometimes freezes up for a few seconds. This is weird because all the messages are loaded once the user scrolls. Any ideas on how to make this fast scrolling no problem?
Thank you!
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *simpleTableIdentifier = #"MailCell";
MailCell *cell = (MailCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MailCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
// Anything that should be the same on EACH cell should be here.
UIView *myBackView = [[UIView alloc] initWithFrame:cell.frame];
myBackView.backgroundColor = [UIColor colorWithRed:40.0/255.0 green:148.0/255.0 blue:196.0/255.0 alpha:1];
cell.selectedBackgroundView = myBackView;
cell.messageText.textAlignment = NSTextAlignmentLeft;
cell.messageText.lineBreakMode = NSLineBreakByTruncatingTail;
}
NSUInteger row = [indexPath row];
// Extract Data
// Use the message object instead of the multiple arrays.
CTCoreMessage *message = [[self allMessages] objectAtIndex:row];
// Sender
CTCoreAddress *sender = [message sender];
NSString *senderName = [sender name];
// Subject
NSString *subject = [message subject];
if ([subject length] == 0)
{
subject = #"(No Subject)";
}
// Body
BOOL isPlain = YES;
NSString *body = [message bodyPreferringPlainText:&isPlain];
body = [[body componentsSeparatedByCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]]
componentsJoinedByString:#" "];
body = [body stringByReplacingOccurrencesOfString:#" " withString:#" "];
// Populate Cell
[[cell nameText] setText:senderName];
[[cell subjectField] setText:subject];
[[cell messageText] setText:body];
if ([message isUnread])
{
cell.nameText.textColor = [UIColor colorWithRed:15.0/255.0 green:140.0/255.0 blue:198.0/255.0 alpha:1];
}
else
{
cell.nameText.textColor = [UIColor blackColor];
}
return cell;
}
xCode comes with a profiler called Instruments. It's CPU time profiler is perfect for figuring out which code is slowing things down. Run your app with the profiler and spend a few seconds just scrolling around. It will give you statistics.
Keep in mind, the code inside if (cell == nil) will run about 10 times (UITableView caches just enough cells to fill itself). But the code outside the if is expensive - it runs every time a cell becomes visible.
I would guess the most expensive operations in the code you posted are:
Giving iOS too many subviews to draw on a cell
Do your own drawing instead.
Replacing runs of whitespace in the entire body text with single spaces
The code you posted allocates new strings for each word, plus an array to hold them. Then it allocates two more copies (one with words rejoined and one with runs of spaces compacted). It processes the entire body text string, even if the majority will never be visible to the user in a tiny preview of the body!
Cache the resulting string so that this operation is performed only once per cell.
Also, you can create a new mutable string, reserve space in it, and copy characters from the original in a loop (except runs of whitespace). Instead of processing the entire body text, you could stop at 100 characters or so (enough to fill a table cell). Faster and saves memory.
Slow UITableView scrolling is a very very common question. See:
How to solve slow scrolling in UITableView
iPhone UITableView stutters with custom cells. How can I get it to scroll smoothly?
Nothing seems wrong with your code. I'd recommend using a table optimization framework such as the free Sensible TableView.
When my custom UITableViewCell is reused, all of the data does not get reset for the current cell. Data from the reused cell is inserted for a particular UILabel.
I have a tableview of Dishes. Dishes have Comments. I'd like to display comments for each dish within a tableviewcell. Depending on how many comments there are, I only make that many comment labels in each cell. However, when a cell is reused, the wrong comment - a comment from a different Dish - shows up.
I am using CoreData and a Dish has many Comments and let's just say I only want to display the first two comments.
I have created two UILabels in IB with heights and width of zero so that it resizes to how long each comment is.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DishCell *cell = [tableView dequeueReusableCellWithIdentifier:#"DishCell"];
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{ ...I put all of the Dishes in a NSFetchedResultsController instance so *dish is a NSManagedObject that has many comments
NSSet *commentsSet = dish.comments;
NSSortDescriptor *sortByDate = [[NSSortDescriptor alloc]initWithKey:#"date" ascending:YES];
NSArray *commentArray = [commentsSet sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortByDate]];
if (commentsSet.count< 1) {
dishCell.comment1label.text = #"";
dishCell.comment2label.text = #"";
}else {
Comment *comment = [commentArray objectAtIndex:0];
NSString *commenterName = comment.displayName;
NSString *commentString = comment.commentText;
NSString *text = [NSString stringWithFormat:#"%#: %#", commenterName, commentString];
CGSize comment1Size = [text sizeWithFont:[UIFont systemFontOfSize:10.0f] constrainedToSize:CGSizeMake(constraint.width - dishCell.commentBubble.frame.size.width + 4, constraint.height) lineBreakMode:UILineBreakModeWordWrap];
//Ignore this setFrame - it works
[dishCell.comment1label setFrame:CGRectMake(32, faveIconYpos - 2 + dishCell.favIcon.frame.size.height, 298 - dishCell.commentBubble.frame.size.width, comment1Size.height + 2)];
dishCell.text = text;
if (commentsSet.count > 1) {
Comment *comment2 = [commentArray objectAtIndex:1];
[dishCell.comment2label setTextColor:[UIColor whiteColor]];
[dishCell.comment2label setFont:[UIFont systemFontOfSize:12.0]];
NSString *text2 = [NSString stringWithFormat:#"%#: %#", comment2.displayName, comment2.commentText];
CGSize comment2Size = [text2 sizeWithFont:[UIFont systemFontOfSize:10.0f] constrainedToSize:CGSizeMake(constraint.width - dishCell.commentBubble.frame.size.width + 4, constraint.height) lineBreakMode:UILineBreakModeWordWrap];
[dishCell.comment2label setFrame:CGRectMake(32, faveIconYpos + dishCell.favIcon.frame.size.height + dishCell.testcom.frame.size.height - 2, 298 - dishCell.commentBubble.frame.size.width, comment2Size.height + 2)];
dishCell.comment2label.text = text2;
}
I've parsed out some of the code, but the main thing is that data from another cell gets inserted into my labels. Why is this happening?
When using the following code to re-size a table row the last line of text is always cutoff, no matter how many lines there are. But there is white space added that looks like enough space for the text.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
CGFloat restOfTheCellHeight = tableView.rowHeight - cell.detailTextLabel.frame.size.height;
CGSize constrainedSize = CGSizeMake(cell.detailTextLabel.frame.size.width, CGFLOAT_MAX);
CGSize textHeight = [cell.detailTextLabel.text sizeWithFont:cell.detailTextLabel.font constrainedToSize:constrainedSize lineBreakMode:cell.detailTextLabel.lineBreakMode];
CGFloat newCellHeight = (textHeight.height + restOfTheCellHeight);
if (tableView.rowHeight > newCellHeight) {
newCellHeight = tableView.rowHeight;
}
return newCellHeight;
}
Here is the code in cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCellTableRowTypeSingleLineValueSmallLabel *cell = (CustomCellTableRowTypeSingleLineValueSmallLabel *)[tableView dequeueReusableCellWithIdentifier:#"CellTypeMultiLineLabelInCellSmallCell"];
if (cell == nil) {
NSArray *xibObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCellTableRowTypeSingleLine" owner:nil options:nil];
for(id currentObject in xibObjects) {
if([currentObject isKindOfClass:[CustomCellTableRowTypeSingleLineValueSmallLabel class]]){
cell = (CustomCellTableRowTypeSingleLineValueSmallLabel *)currentObject;
}
}
cell.accessoryType = UITableViewCellAccessoryNone;
cell.editingAccessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.detailTextLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.detailTextLabel.numberOfLines = 0;
cell.detailTextLabel.text = self.attributeStringValue;
cell.textLabel.text = self.rowLabel;
return cell;
}
Any ideas?
You need to call [cell.detailTextLabel sizeToFit] in order for the label to actually resize in cellForRowAtIndexPath. It will not resize on its own just because you set numberOfLines to 0. See this question and read its answers for more clarification.
You are calculating the cell height appropriately in your heightForRowAtIndexPAth method, but then in your cellForRowAtIndexPath method you are never actually using it to set the height of your label within it.
So the table is allocating the right amount of space based on your heightForRowAtIndexPath, but then inserting into that space the unresized cell that you return from cellForRowAtIndexPath. I think this might the the cause of the problem and would explain the results you are seeing.
In cellForRowAtIndexPath you need to actually set the height of the label using the same calculation.
i.e.
CGSize constrainedSize = CGSizeMake(cell.detailTextLabel.frame.size.width, CGFLOAT_MAX);
CGRect cframe = cell.detailTextLabel.frame;
cframe.size.height = constrainedSize.height;
cell.detailTextLabel.frame = cframe;
You may also need to actually set the content view frame as well (not sure how it works with a non-custom cell).
I'm also not sure its a good idea to be calling cellForRowAtIndexPath from the heightForRowAtIndexPath method (it would probably be better to just directly access the text data you are using for the size calculation directly).
Turns out I just needed to enable all of the Autosizing options in interface builder for the label.
I have a UILabel that I create a radius on the layer, using cornerRadius. The ultimate goal is to make the label look like Apple does in the mail app.
It looks great at first, but once you drill down into that row and back a few times, the quality of the rounded edge starts to degrade. You can see in the screen shot, the left side is blocky.
Why would this be happening? It seems to happen after about 2 times of loading that view.
(source: puc.edu)
Here is my cell creation method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
Trip *trip = [fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = trip.title;
cell.detailTextLabel.text = #"Date of trip";
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
// Create a nice number, like mail uses
UILabel *count = [[UILabel alloc] initWithFrame:CGRectMake(cell.contentView.frame.size.width - 50, 12, 34, 20)];
[count setText:[NSString stringWithFormat:#"%i",[[trip.rides allObjects] count]]];
[count setFont:[UIFont fontWithName:#"Helvetica-Bold" size:16]];
count.textAlignment = UITextAlignmentCenter;
count.backgroundColor = [UIColor grayColor];
count.textColor = [UIColor whiteColor];
count.layer.cornerRadius = 10;
[cell addSubview:count];
[count release];
return cell;
}
In every call to cellForRowAtIndexPath, you are creating a new count UILabel. Eventually there will be several overlapping views in the same place with the same antialiased curve, so it will look blocky.
Try creating the count UILabel only when a new cell is created, in the if (cell == nil) block. Otherwise, get the count label by tag.
if ( cell == nil ) {
cell = ...
count = ...
...
count.tag = 'coun';
[cell.contentView addSubview:count];
[count release];
} else {
count = (UILabel *)[cell viewWithTag:'coun'];
}
[count setText:[NSString stringWithFormat:#"%i",[[trip.rides allObjects] count]]];
Check to see if the tableview/cell is set to clear its context before drawing. I noticed I had similar issues with text on the cell.
I've seen some strange issues wherein it looks like Core Animation based properties are accumulative even though they shouldn't be. Your problem here might be caused by some kind of accumulative creep from changing the value of the corner radius repeatedly every time the cell is returned.
I would suggest testing if the corner radius already equals 10 before setting it again. (Although I would expect that to show up more with scrolling up and down than in reloading the view.)
Another possible problem would be that some subview is migrating causing a visual artifact.
Edit:
Instead of adding the label to the cell itself. Try adding it as a subview of the cell's content view.