My app pulls HTML from an API, converts it into a NSAttributedString
(in order to allow for tappable links) and writes it to a row in an AutoLayout table. Trouble is, any time I invoke this type of cell, the height is miscalculated and the content is cut off. I have tried different implementations of row height calculations, none of which work correctly.
How can I accurately, and dynamically, calculate the height of one of these rows, while still maintaining the ability to tap HTML links?
My code is below.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
switch(indexPath.section) {
...
case kContent:
{
FlexibleTextViewTableViewCell* cell = (FlexibleTextViewTableViewCell*)[TableFactory getCellForIdentifier:@"content" cellClass:FlexibleTextViewTableViewCell.class forTable:tableView withStyle:UITableViewCellStyleDefault];
[self configureContentCellForIndexPath:cell atIndexPath:indexPath];
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.desc.font = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];
return cell;
}
...
default:
return nil;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *contentFont = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];
switch(indexPath.section) {
...
case kContent:
return [self textViewHeightForAttributedText:[self convertHTMLtoAttributedString:myHTMLString] andFont:contentFont andWidth:self.tappableCell.width];
break;
...
default:
return 0.0f;
}
}
-(NSAttributedString*) convertHTMLtoAttributedString: (NSString *) html {
return [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding]
options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)}
documentAttributes:nil
error:nil];
}
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width {
NSMutableAttributedString *mutableText = [[NSMutableAttributedString alloc] initWithAttributedString:text];
[mutableText addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, text.length)];
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:mutableText];
CGSize size = [self text:mutableText.string sizeWithFont:font constrainedToSize:CGSizeMake(width,FLT_MAX)];
CGSize sizeThatFits = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return sizeThatFits.height;
}
You need to update intrinsic content size.
I assume that you set attributed text to label in this code
[self configureContentCellForIndexPath:cell atIndexPath:indexPath];
So, it should look like this
You height calculation code
(CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width
should be replaced with cell height calculation using prototyping cell.In the app I'm working on, the app pulls terrible HTML strings from a lousy API written by other people and converts HTML strings to
NSAttributedString
objects. I have no choice but to use this lousy API. Very sad. Anyone who has to parse terrible HTML string knows my pain. I useText Kit
. Here is how:NSAttributedString
object, use custom attribute to mark links, useNSTextAttachment
to mark images. I call it rich text.Text Kit
objects. i.e.NSLayoutManager
,NSTextStorage
,NSTextContainer
. Hook them up after allocation.NSTextStorage
object in step 3. with[NSTextStorage setAttributedString:]
[NSLayoutManager ensureLayoutForTextContainer:]
to force layout to happen[NSLayoutManager usedRectForTextContainer:]
. Add padding or margin if needed.[tableView: heightForRowAtIndexPath:]
[NSLayoutManager drawGlyphsForGlyphRange:atPoint:]
. I use off-screen drawing technique here so the result is anUIImage
object.UIImageView
to render the final result image. Or pass the result image object to thecontents
property oflayer
property ofcontentView
property ofUITableViewCell
object in[tableView:cellForRowAtIndexPath:]
.[NSLayoutManager glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph]
and[NSAttributedString attribute:atIndex:effectiveRange:]
.Event handling code snippet:
This approach is much faster and more customizable than
[NSAttributedString initWithData:options:documentAttributes:error:]
when rendering same html string. Even without profiling I can tell theText Kit
approach is faster. It's very fast and satisfying even though I have to parse html and construct attributed string myself. TheNSDocumentTypeDocumentAttribute
approach is too slow thus is not acceptable. WithText Kit
, I can also create complex layout like text block with variable indentation, border, any-depth nested text block, etc. But it does need to write more code to constructNSAttributedString
and to control layout process. I don't know how to calculate the bounding rect of an attributed string created withNSDocumentTypeDocumentAttribute
. I believe attributed strings created withNSDocumentTypeDocumentAttribute
are handled byWeb Kit
instead ofText Kit
. Thus is not meant for variable height table view cells.EDIT: If you must use
NSDocumentTypeDocumentAttribute
, I think you have to figure out how the layout process happens. Maybe you can set some breakpoints to see what object is responsible for layout process. Then maybe you can query that object or use another approach to simulate the layout process to get the layout information. Some people use an ad-hoc cell or aUITextView
object to calculate height which I think is not a good solution. Because in this way, the app has to layout the same chunk of text at least twice. Whether you know or not, somewhere in your app, some object has to layout the text just so you can get information of layout like bounding rect. Since you mentionedNSAttributedString
class, the best solution isText Kit
after iOS 7. OrCore Text
if your app is targeted on earlier iOS version.I strongly recommend
Text Kit
because in this way, for every html string pulled from API, the layout process only happens once and layout information like bounding rect and positions of every glyph are cached byNSLayoutManager
object. As long as theText Kit
objects are kept, you can always reuse them. This is extremely efficient when using table view to render arbitrary length text because text are laid out only once and drawn every time a cell is needed to display. I also recommend useText Kit
withoutUITextView
as the official apple docs suggested. Because one must cache everyUITextView
if he wants to reuse theText Kit
objects attached with thatUITextView
. AttachText Kit
objects to model objects like I do and only updateNSTextStorage
and forceNSLayoutManager
to layout when a new html string is pulled from API. If the number of rows of table view is fixed, one can also use a fixed list of placeholder model objects to avoid repeat allocation and configuration. And becausedrawRect:
causesCore Animation
to create useless backing bitmap which must be avoided, do not useUIView
anddrawRect:
. Either useCALayer
drawing technique or draw text into a bitmap context. I use the latter approach because that can be done in a background thread withGCD
, thus the main thread is free to respond to user's operation. The result in my app is really satisfying, it's fast, the typesetting is nice, the scrolling of table view is very smooth (60 fps) since all the drawing process are done in background threads withGCD
. Every app needs to draw some text with table view should useText Kit
.You can replace this method to calculate the height of attributed string:
}
Maybe the font you changed doesnt matches with the font of content on html pages. So, use this method to create attributed string with appropriate font:
// HTML -> NSAttributedString
}
// force font thrugh & css
}
and in your tableView:heightForRowAtIndexPath: replace it with this:
I ran into a very similar issue on another project where fields using NSAttributedString weren't rendering with the correct height. Unfortunately, there are two bugs with it that made us completely drop using it in our project.
The first is a bug that you've noticed here, where some HTML will cause an incorrect size calculation. This is usually from the space between the p tags. Injecting CSS sort of solved the issue, but we had no control over the incoming format. This behaves differently between iOS7 and iOS8 where it's wrong on one and right on the other.
The second (and more serious) bug is that NSAttributedString is absurdly slow in iOS 8. I outlined it here: NSAttributedString performance is worse under iOS 8
Rather than making a bunch of hacks to have everything perform as we wanted, the suggestion of using https://github.com/Cocoanetics/DTCoreText worked out really well for the project.
[cell.descriptionLabel setPreferredMaxLayoutWidth:375.0];
I'm assuming you are using a
UILabel
to display the string?If you are, I have had countless issues with multiline labels with autoLayout. I provided an answer here
Table View Cell AutoLayout in iOS8
which also references another answer of mine that has a breakdown of how i've solved all my issues. Similar issues have cropped up again in iOS 8 that require a similar fix in a different area.
All comes down to the idea of setting the
UILabel
'spreferredMaxLayoutWidth
every time is bounds change. What also helped is setting the cells width to be the width of the tableview before running: