AutoLayout行高错误计算NSAttributedString

我的应用程序从一个API拉HTML,将其转换成一个NSAttributedString (为了允许可点击的链接),并将其写入到AutoLayout表中的一行。 麻烦的是,任何时候我调用这种types的单元格,高度是错误的计算和内容被切断。 我已经尝试了行高计算的不同实现,其中没有一个正常工作。

我怎样才能准确地,dynamic地计算这些行之一的高度,同时仍然保持点击HTML链接的能力?

不希望的行为的例子

我的代码如下。

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

在我正在开发的应用程序中,应用程序从其他人编写的糟糕的API中NSAttributedString可怕的HTMLstring,并将HTMLstring转换为NSAttributedString对象。 我别无select,只能使用这个糟糕的API。 很伤心。 任何需要parsing可怕的HTMLstring的人都知道我的痛苦。 我使用Text Kit 。 这里是如何:

  1. parsinghtmlstring来获取DOM对象。 我用一个轻的包装, hpple使用libxml。 这个组合是超快速和易于使用的。 powershell推荐。
  2. 遍历DOM对象构造NSAttributedString对象,使用自定义属性标记链接,使用NSTextAttachment标记图像。 我把它称为富文本。
  3. 创build或重用主要的Text Kit对象。 即NSLayoutManagerNSTextStorageNSTextContainer 。 把它们挂在分配后。
  4. 布局过程
    1. 将步骤2中构build的富文本传递给步骤3中的NSTextStorage对象,并使用[NSTextStorage setAttributedString:]
    2. 使用方法[NSLayoutManager ensureLayoutForTextContainer:]强制布局发生
  5. 使用方法[NSLayoutManager usedRectForTextContainer:]计算绘制富文本所需的框架。 如果需要,添加填充或边距。
  6. 渲染过程
    1. 返回[tableView: heightForRowAtIndexPath:]中步骤5中计算的高度
    2. [NSLayoutManager drawGlyphsForGlyphRange:atPoint:]在步骤2中绘制富文本。 我在这里使用屏幕外的绘图技术,所以结果是一个UIImage对象。
    3. 使用UIImageView来渲染最终的结果图像。 或者将结果图像对象传递给[tableView:cellForRowAtIndexPath:]UITableViewCell对象的contentView属性的layer属性的contentView属性。
  7. 事件处理
    1. 捕捉触摸事件。 我使用附在桌子视图上的轻击手势识别器。
    2. 获取触摸事件的位置。 使用此位置来检查用户是否点击链接或图像与[NSLayoutManager glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph][NSAttributedString attribute:atIndex:effectiveRange:]

事件处理代码片段:

 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 

当呈现相同的htmlstring时,这种方法比[NSAttributedString initWithData:options:documentAttributes:error:]更快,更可定制。 即使没有分析我可以告诉Text Kit方法是更快。 这是非常快速和令人满意的,即使我必须parsingHTML和构build属性string我自己。 NSDocumentTypeDocumentAttribute方法太慢,因此是不可接受的。 使用Text Kit ,我也可以创build具有variables缩进,边框,任意深度嵌套文本块等的复杂布局,但是它需要编写更多代码来构造NSAttributedString并控制布局过程。 我不知道如何计算用NSDocumentTypeDocumentAttribute创build的属性string的边界矩形。 我相信用NSDocumentTypeDocumentAttribute创build的属性string是由Web Kit而不是Text Kit 。 因此不适用于可变高度表格视图单元格。

编辑:如果您必须使用NSDocumentTypeDocumentAttribute ,我认为你必须弄清楚布局过程如何发生。 也许你可以设置一些断点来查看哪个对象负责布局过程。 然后,也许你可以查询该对象或使用另一种方法来模拟布局过程来获取布局信息。 有些人使用专门的单元格或UITextView对象来计算高度,我认为这不是一个好的解决scheme。 因为这样,应用程序必须至less布置两次相同的文本块。 无论你是否知道,在你的应用程序的某个地方,一些对象不得不布局文本,所以你可以得到像边界矩形布局的信息。 由于您提到了NSAttributedString类,所以最好的解决scheme是在iOS 7之后的Text Kit 。或者如果您的应用程序针对早期的iOS版本,则为Core Text

我强烈build议使用Text Kit因为通过这种方式,对于从API中抽取的每个HTMLstring,布局过程只发生一次,像NSLayoutManager对象那样caching每个字形的边界矩形和位置等布局信息。 只要Text Kit对象被保留,您就可以随时重用它们。 当使用表视图渲染任意长度的文本时,这是非常有效的,因为文本只布置一次,每次需要显示单元格时都会绘制。 我也build议使用没有UITextView Text Kit作为正式的苹果文档build议。 因为必须caching每个UITextView如果他想重用与该UITextView附加的Text Kit对象。 附加Text Kit对象来模拟对象像我一样,只有更新NSTextStorage和强制NSLayoutManager布局时,从API拉新的HTMLstring。 如果表视图的行数是固定的,则还可以使用固定的占位符模型对象列表来避免重复分配和configuration。 而且因为drawRect:导致Core Animation创build无用的后台位图,所以必须避免,不要使用UIViewdrawRect: 使用CALayer绘图技术或将文本绘制到位图上下文中。 我使用后一种方法,因为这可以在GCD的后台线程中完成,因此主线程可以自由地响应用户的操作。 在我的应用程序的结果是非常令人满意的,它的速度很快,排版很好,表视图的滚动是非常平滑的(60 fps),因为所有的绘图过程都是在GCD后台线程中完成的。 每个应用程序需要使用表格视图绘制一些文本应使用Text Kit

我假设你正在使用UILabel来显示string?

如果是的话,我用autoLayout有多行标签的问题。 我在这里提供了一个答案

在iOS8中查看表格单元格自动布局

这也引用了我的另一个答案,如何解决我所有的问题。 在iOS 8中又出现了类似的问题,需要在不同的领域进行类似的修复。

所有归结为每次设置UILabelpreferredMaxLayoutWidth UILabel的想法都是有变化的。 还有一个帮助就是在运行前将单元格宽度设置为tableview的宽度:

 CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; 

我遇到了一个非常类似的问题,在另一个项目中,使用NSAttributedString的字段没有使用正确的高度进行渲染。 不幸的是,它有两个错误,使我们完全放弃使用它在我们的项目。

首先是您在这里注意到的一个错误,其中一些HTML会导致错误的大小计算。 这通常来自p标签之间的空间。 注入CSSsorting解决了这个问题,但我们无法控制传入的格式。 这在iOS7和iOS8之间的行为是不一样的,它们之间是错误的,而另一方面则是错误的。

第二个(也是更严重的)错误是NSAttributedString在iOS 8中的速度非常慢。我在这里概括了一下: 在iOS 8下,NSAttributedString的性能更差

而不是使一堆黑客按照我们的想法执行所有的事情,使用https://github.com/Cocoanetics/DTCoreText的build议对于这个项目非常有效。

您需要更新固有内容大小。

我假设你设置属性文本标签在这个代码[self configureContentCellForIndexPath:cell atIndexPath:indexPath];

所以,它应该是这样的

 cell.youLabel.attributedText = NSAttributedString(...) cell.youLabel.invalidateIntrinsicContentSize() cell.youLabel.layoutIfNeeded() 

您使用原型单元格将高度计算代码(CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)widthreplace为单元格高度计算。

如果您可以使用dynamic单元大小来定位iOS 8,则是解决您的问题的理想select。

要使用dynamic单元格大小,请删除heightForRowAtIndexPath:并将self.tableView.rowHeight设置为UITableViewAutomaticDimension。

这里是一个video更多的细节: https : //developer.apple.com/videos/wwdc/2014/?include=226#226

您可以replace此方法来计算属性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; 

}

也许你改变的字体不符合html页面上的内容的字体。 所以,使用这种方法创build适当的字体的属性的string:

// 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; 

}

//强制字体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]]; 

}

并在你的tableView:heightForRowAtIndexPath:replace它:

 case kContent: return [self textViewHeightForAttributedText:[self attributedStringFromHTML:myHTMLString withFont:contentFont] andFont:contentFont andWidth:self.tappableCell.width]; break; 

你应该能够转换成NSString来计算这个高度。

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