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 i
You can replace this method to calculate the height of attributed string:
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width {
CGFloat result = font.pointSize + 4;
if (text)
result = (ceilf(CGRectGetHeight([text boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil])) + 1);
return result;
}
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
-(NSAttributedString*) convertHTMLtoAttributedString: (NSString *) html {
NSError *error;
NSDictionary *options = @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:nil error:&error];
if(!attrString) {
NSLog(@"creating attributed string from HTML failed: %@", error.debugDescription);
}
return attrString;
}
// force font thrugh & css
- (NSAttributedString *)attributedStringFromHTML:(NSString *)html withFont:(UIFont *)font {
return [self convertHTMLtoAttributedString:[NSString stringWithFormat:@"<span style=\"font-family: %@; font-size: %f\";>%@</span>", font.fontName, font.pointSize, html]];
}
and in your tableView:heightForRowAtIndexPath: replace it with this:
case kContent:
return [self textViewHeightForAttributedText:[self attributedStringFromHTML:myHTMLString withFont:contentFont] andFont:contentFont andWidth:self.tappableCell.width];
break;
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
's preferredMaxLayoutWidth
every time is bounds change. What also helped is setting the cells width to be the width of the tableview before running:
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
You should be able to convert to an NSString to calculate the height like this.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIFont * font = [UIFont systemFontOfSize:15.0f];
NSString *text = [getYourAttributedTextArray objectAtIndex:indexPath.row] string];
CGFloat height = [text boundingRectWithSize:CGSizeMake(self.tableView.frame.size.width, maxHeight) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) attributes:@{NSFontAttributeName: font} context:nil].size.height;
return height + additionalHeightBuffer;
}
[cell.descriptionLabel setPreferredMaxLayoutWidth:375.0];
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 use Text Kit
. Here is how:
NSAttributedString
object, use custom attribute to mark links, use NSTextAttachment
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 an UIImage
object.UIImageView
to render the final result image. Or pass the result image object to the contents
property of layer
property of contentView
property of UITableViewCell
object in [tableView:cellForRowAtIndexPath:]
.[NSLayoutManager glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph]
and [NSAttributedString attribute:atIndex:effectiveRange:]
. Event handling code snippet:
CGPoint location = [tap locationInView:self.tableView];
// tap is a tap gesture recognizer
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (!indexPath) {
return;
}
CustomDataModel *post = [self getPostWithIndexPath:indexPath];
// CustomDataModel is a subclass of NSObject class.
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
location = [tap locationInView:cell.contentView];
// the rich text is drawn into a bitmap context and rendered with
// cell.contentView.layer.contents
// The `Text Kit` objects can be accessed with the model object.
NSUInteger index = [post.layoutManager
glyphIndexForPoint:location
inTextContainer:post.textContainer
fractionOfDistanceThroughGlyph:NULL];
CustomLinkAttribute *link = [post.content.richText
attribute:CustomLinkAttributeName
atIndex:index
effectiveRange:NULL];
// CustomLinkAttributeName is a string constant defined in other file
// CustomLinkAttribute is a subclass of NSObject class. The instance of
// this class contains information of a link
if (link) {
// handle tap on link
}
// same technique can be used to handle tap on image
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 the Text Kit
approach is faster. It's very fast and satisfying even though I have to parse html and construct attributed string myself. The NSDocumentTypeDocumentAttribute
approach is too slow thus is not acceptable. With Text 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 construct NSAttributedString
and to control layout process. I don't know how to calculate the bounding rect of an attributed string created with NSDocumentTypeDocumentAttribute
. I believe attributed strings created with NSDocumentTypeDocumentAttribute
are handled by Web Kit
instead of Text 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 a UITextView
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 mentioned NSAttributedString
class, the best solution is Text Kit
after iOS 7. Or Core 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 by NSLayoutManager
object. As long as the Text 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 use Text Kit
without UITextView
as the official apple docs suggested. Because one must cache every UITextView
if he wants to reuse the Text Kit
objects attached with that UITextView
. Attach Text Kit
objects to model objects like I do and only update NSTextStorage
and force NSLayoutManager
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 because drawRect:
causes Core Animation
to create useless backing bitmap which must be avoided, do not use UIView
and drawRect:
. Either use CALayer
drawing technique or draw text into a bitmap context. I use the latter approach because that can be done in a background thread with GCD
, 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 with GCD
. Every app needs to draw some text with table view should use Text Kit
.
If you can target iOS 8 using dynamic cell sizing is the ideal solution to your problem.
To use dynamic cell sizing, delete heightForRowAtIndexPath: and set self.tableView.rowHeight to UITableViewAutomaticDimension.
Here is a video with more details: https://developer.apple.com/videos/wwdc/2014/?include=226#226