需求背景
产品需要展示html格式的富文本字符串,这个html是用户可编辑的,但不能有交互行为。从设计稿上看,这段富文本字符串需要展示在一个tableViewCell里,并且得让其自适应高度。
使用UILabel去实现需求
实际上,UILabel是支持attributeString的,而attributeString又可以通过html字符串生成,如下:
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]
initWithData:[htmlStr
dataUsingEncoding:NSUTF8StringEncoding]
options:@{
NSDocumentTypeDocumentAttribute :
NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute :
@(NSUTF8StringEncoding)
}
documentAttributes:nil
error:nil];
而UILabel的自适应高度相对容易实现一些(配合自动布局),而且这个需求不需要这段html跟用户有交互,所以一开始是用UILabel实现的。
但是这个方法有两个坑,第一个是不能显示表格的边框。像以下的代码是渲染不出border的:
<table border="1">
<tr>
<th>Month</th>
<th>Savings</th>
</tr>
<tr>
<td>January</td>
<td>$100</td>
</tr>
</table>
这个坑是iOS的系统bug,随便给富文本加一个属性,就能看到这个边框了:
//先判断字符串长度,以免越界崩溃
if(attrStr.length>0){
[attrStr addAttribute:NSBackgroundColorAttributeName value:[UIColor clearColor] range:NSMakeRange(0, 1)];
}
第二个坑,就是无法渲染渐变字的文本,类似于这种:
<span style="-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(#FF0000, #F7B62E, #F7B62E, #3EACFF, #A3E880 );color: transparent;font-family: Chalkduster; font-size: 30px;">
<i>grandma calls me an artist</i>
</span>
这样的文本用NSMutableAttributedString渲染,字体就只能是黑色的了,没有把渐变色渲染出来。因此要想完美实现这个需求,还得用webview
WKWebView实现需求
使用WKWebViewweb加载htmL并不难,系统已经提供了对应的方法:
[self.wkWebView loadHTMLString:self.htmlString baseURL:nil];
尽管有这一方法,但是这个需求实现下来还有一系列的坑,下面一一说下
在xib文件中使用webView
xib确实有wkWebView的控件,但是是ios11才能用的,因此只能用一个UIView,在这里面封装WebView(或者也可以指定该UIView的类名为WKWebView,由于我还需自定义WKWebView的特性,因此选择用封装的方式)。此外,wkWebView的自适应高度代表UITableViewCell自适应高度,需要UITableViewCell的垂直上的约束线连在一起。因此需要给wkWebView设定高度的约束。
得到wkwebView的文本高度
网上找了一圈,发现wkwebView得到文本高度可以通过两种方法。一种是用KVO监听ScrollView的contentSize,另外就是运行获取高度(document.body.scrollHeight)的JS脚本,由于这次的需求只是简单地展示html,两种方法获取高度都没有差异的,而且都是异步的。但是这里有个坑:由于这个需求是能让用户可以html,因此webView在其生命周期内可以加载多次html。再改动html后,获得的高度不准了(得到的高度越来越大),后来发现是似乎因为wkwebView没有适配屏幕大小,于是在其初始化的时候加了一段代码:
WKWebViewConfiguration *config = [WKWebViewConfiguration new];
NSString *script = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUserScript = [[WKUserScript alloc] initWithSource:script injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUserController addUserScript: wkUserScript];
config.userContentController = wkUserController;
self.webView = [[WKWebView alloc]initWithFrame:self.bounds configuration:config];
这样高度就不会越来越大了,此外这段代码还能解决wkwebView里面的字体过小的问题。
但是,仅仅这样是不够的,因为这两种方式得到的高度都不小于wkWebView的高度。如果给wkWebView设置过大的文本,然后又通过编辑给其设置较小的高度。这样wkWebView的高度不会减少。我的解决方法就是在wkWebView设置文本的时候重新将其高度置为0,在得到高度后才设回去。总体代码如下:
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL{
//高度置0
self.wkWebView.height = 0;
[self.wkWebView loadHTMLString: string baseURL:nil];
}
-(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[self.wkWebView evaluateJavaScript:@"document.documentElement.scrollHeight" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
CGFloat height = [result doubleValue];
self.wkWebView.height = height;
//通知cell更改约束
}];
}
获取高度后更新视图
获取高度是异步的,因此获取之后需要通知Cell更新约束,再用Cell通知tableView刷新,由于只是更新了约束,可以只用beginUpdate/endUpdate方法即可,这样可以避免[tableView reloadData]->[wkWebView loadHTMLString]->[self.wkWebView evaluateJavaScript]->[tableView reloadData]这样的循环调用。