DTCoreText是可以將HTML字符串轉(zhuǎn)化為富文本使用的工具,既保證原生實(shí)現(xiàn)又能適應(yīng)靈活的樣式修改,而且相比于使用WebView顯示內(nèi)容在性能上也有很大優(yōu)勢。本篇就這一技術(shù)的使用進(jìn)行總結(jié)。
目錄
一、相關(guān)資源
二、DTCoreText的集成
三、DTCoreText的使用
四、可能遇到的錯誤
五、參考鏈接
一、相關(guān)資源
溫馨提示:文中代碼量比較大,看起來可能比較費(fèi)勁,所以先將本文的Demo鏈接放在這里,結(jié)合代碼看更方便,畢竟代碼才是硬道理嘛!
二、DTCoreText的集成
在項(xiàng)目中使用DTCoreText需要它的兩個靜態(tài)庫:DTCoreText.framework和DTFoundation,framework。但是從Github下載的文件卻不能直接使用。起初我是直接從網(wǎng)上其他地方下載打包好的靜態(tài)庫來使用的,但這樣會遺漏掉更新。所以還是總結(jié)了集成DTCoreText的具體步驟如下:
1.下載源碼
創(chuàng)建一個文件夾DTCoreText,使用終端命令下載源碼:
git clone --recursive [https://github.com/Cocoanetics/DTCoreText.git](https://github.com/Cocoanetics/DTCoreText.git) Externals/DTCoreText
2.設(shè)置編譯配置
使用下載的工程可以根據(jù)需求修改編譯配置,然后運(yùn)行工程得到適合不同平臺的靜態(tài)庫。為了獲取到適合iOS設(shè)備使用的SDK,我們選擇工程Target為DTCoreText(iOS),并依次選擇工程->TARGETS->DTCoreText(iOS)->Deployment Info,選擇需要最低支持的系統(tǒng)版本

3.設(shè)置Scheme
將Run環(huán)境修改為Release模式,依次選擇Product->Scheme->Edit Scheme:

4.模擬器和真機(jī)分別編譯
使用commadn+B執(zhí)行代碼編譯,注意一定要選擇正確的Target,我們既然是用于iOS,就應(yīng)該選擇DTCoreText (iOS)這個Target進(jìn)行編譯


5.打開編譯緩存,獲取靜態(tài)庫
在Xcode依次打開File->File Setting->DerivedData,然后點(diǎn)擊之后的路徑箭頭可以找到DTCoreText的編譯緩存文件夾

然后我們再依次打開Build->Products,將會看到適合模擬器和真機(jī)使用的兩個靜態(tài)庫文件夾:

6.合并靜態(tài)庫
經(jīng)過上述步驟,我分別得到了適合模擬器和真機(jī)使用的靜態(tài)庫,現(xiàn)在我們把它們合并。首先我們把Products文件拷貝到一個合適的位置,然后在控制臺執(zhí)行合并靜態(tài)庫的命令如下:

如圖,我們使用lipo -create命令合并了適合模擬器和真機(jī)使用的可執(zhí)行文件并放在了桌面上如下:

然后我們需要將兩個文件分別對應(yīng)拷貝到Release-iphones文件夾中的DTCoreText.framework和DTFoundation.framework中。這樣最終我們就得到想要的靜態(tài)庫。
三、DTCoreText的使用
在這之前我們準(zhǔn)備一段測試用的Html如下:
_html = @"<span style=\"color:#333;font-size:15px;\"><strong>標(biāo)題1</strong></span><br/><span align=\"left\" style=\"color:#333;font-size:15px;\">詳細(xì)介紹詳細(xì)介紹詳細(xì)介紹詳細(xì)介紹詳細(xì)介紹詳細(xì)介紹。</span><br/><img src=\"http://cn-qinqimaifang-uat.oss-cn-hangzhou.aliyuncs.com/img/specialist/upload/spcetiicwlz1v_54e2e00fa8a6faf66168571654dbfee2.jpg\" _src=\"http://cn-qinqimaifang-uat.oss-cn-hangzhou.aliyuncs.com/img/specialist/upload/spcetiicwlz1v_54e2e00fa8a6faf66168571654dbfee2.jpg\"><br/><br/><span style=\"color:#333;font-size:15px;\">百度:<a href=\"http://www.baidu.com.cn\">my testlink</a></span><br/><br/><span style=\"color:#333;font-size:15px;\">電話:<a href=\"tel:4008001234\">my phoneNum</a></span><br/><br/><span style=\"color:#333;font-size:15px;\">我郵箱:<a href=\"mailto:dreamcoffeezs@163.com\">my mail</a></span>";
關(guān)于DTCoreText,我們主要用到它的三個控件,分別是DTAttributedLabel,DTAttributedTextView和DTAttributedTextCell。下面對它們的使用進(jìn)行具體說明:
1.DTAttributedLabel
顧名思義,我們也會把DTAttributedLabel當(dāng)做一個UILabel來使用(雖然事實(shí)上DTAttributedLabel并不屬于UILabel的子類)。這里針對DTAttributedLabel使用的三個問題進(jìn)行介紹。
1.1顯示基本文本
//1.創(chuàng)建DTAttributedLabel
_attributedLabel = [[DTAttributedLabel alloc] initWithFrame:CGRectZero];
_attributedLabel.delegate = self;
[self.view addSubview:self.attributedLabel];
//2.Html轉(zhuǎn)化富文本
NSData *data = [_html dataUsingEncoding:NSUTF8StringEncoding];
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithHTMLData:data documentAttributes:NULL];
//3.計(jì)算Frame
//預(yù)設(shè)一個最大的Frame,限寬不限高,方便以后計(jì)算布局
_viewMaxRect = CGRectMake(15, 15, ZSToolScreenWidth - 15*2, CGFLOAT_HEIGHT_UNKNOWN);
DTCoreTextLayouter *layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:attributedString];
NSRange entireStringRange = NSMakeRange(0, [attributedString length]);
DTCoreTextLayoutFrame *layoutFrame = [layouter layoutFrameWithRect:_viewMaxRect range:entireStringRange];
CGSize textSize = [layoutFrame frame].size;
self.attributedLabel.frame = CGRectMake(_viewMaxRect.origin.x, _viewMaxRect.origin.y, _viewMaxRect.size.width, textSize.height);
//4.設(shè)置富文本
self.attributedLabel.attributedString = [self getAttributedStringWithHtml:self.html];
1.2顯示圖片
既然是富文本,DTAttributedLabel也會顯示圖片。但是如果我們的Html字符串里圖片鏈接沒有包含大小,圖片并不能正常顯示。為了顯示圖片我們需要懶加載獲取寬高再刷新顯示,這就需要用到DTAttributedLabel的代理方法如下:
#pragma mark - Delegate:DTAttributedTextContentViewDelegate
//DTCoretText在解析Html的時候,如果遇到網(wǎng)絡(luò)圖片會插入一個占位符
//對于圖片類型(DTImageTextAttachmentd)的占位符,我們使用DTLazyImageView來顯示
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttachment:(DTTextAttachment *)attachment frame:(CGRect)frame{
if([attachment isKindOfClass:[DTImageTextAttachment class]]){
NSString *imageURL = [NSString stringWithFormat:@"%@", attachment.contentURL];
DTLazyImageView *imageView = [[DTLazyImageView alloc] initWithFrame:frame];
imageView.delegate = self;//圖片懶加載代理
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.image = [(DTImageTextAttachment *)attachment image];
imageView.url = attachment.contentURL;
//處理gif圖片
if ([imageURL containsString:@"gif"]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *gifData = [NSData dataWithContentsOfURL:attachment.contentURL];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = DTAnimatedGIFFromData(gifData);
});
});
}
return imageView;
}
return nil;
}
#pragma mark Delegate:DTLazyImageViewDelegate
//懶加載圖片代理
- (void)lazyImageView:(DTLazyImageView *)lazyImageView didChangeImageSize:(CGSize)size {
NSURL *url = lazyImageView.url;
CGSize imageSize = size;
NSPredicate *pred = [NSPredicate predicateWithFormat:@"contentURL == %@", url];
BOOL didUpdate = NO;
//update all attachments that match this URL (possibly multiple images with same size)
//更新所有匹配URL的占位符視圖
for (DTTextAttachment *oneAttachment in [self.attributedLabel.layoutFrame textAttachmentsWithPredicate:pred]){
//update attachments that have no original size, that also sets the display size
//更新沒有自帶尺寸的占位視圖(如網(wǎng)絡(luò)圖片)
if (CGSizeEqualToSize(oneAttachment.originalSize, CGSizeZero)){
//原始圖片不帶寬高,被識別后修改Html
oneAttachment.originalSize = imageSize;
//使用新拿到的圖片尺寸更新HTML字符串
[self configNoSizeImageView:url.absoluteString size:imageSize];
didUpdate = YES;
}
}
if (didUpdate){
//重新顯示富文本
CGSize textSize = [self getAttributedTextHeightHtml:self.html with_viewMaxRect:_viewMaxRect];
self.attributedLabel.frame = CGRectMake(_viewMaxRect.origin.x, _viewMaxRect.origin.y, _viewMaxRect.size.width, textSize.height);
self.attributedLabel.attributedString = [self getAttributedStringWithHtml:self.html];
self.attributedLabel.layouter = nil;
[self.attributedLabel relayoutText];
}
}
#pragma mark - private Methods
//使用得到的新圖片尺寸,更新HtmlString字符串
- (void)configNoSizeImageView:(NSString *)url size:(CGSize)size{
//_viewMaxRect是預(yù)設(shè)的最大Frame
CGFloat imgSizeScale = size.height/size.width;
CGFloat widthPx = _viewMaxRect.size.width;
CGFloat heightPx = widthPx * imgSizeScale;
NSString *imageInfo = [NSString stringWithFormat:@"_src=\"%@\"",url];
NSString *sizeString = [NSString stringWithFormat:@"style=\"width:%.fpx; height:%.fpx;\"",widthPx,heightPx];
NSString *newImageInfo = [NSString stringWithFormat:@"_src=\"%@\"%@",url,sizeString];
if ([self.html containsString:imageInfo]) {
NSString *newHtml = [self.html stringByReplacingOccurrencesOfString:imageInfo withString:newImageInfo];
self.html = newHtml;
}
}
//使用HtmlString,和預(yù)設(shè)的Frame,計(jì)算富文本視圖自適應(yīng)后的高度
- (CGSize)getAttributedTextHeightHtml:(NSString *)htmlString with_viewMaxRect:(CGRect)_viewMaxRect{
//獲取富文本
NSAttributedString *attributedString = [self getAttributedStringWithHtml:htmlString];
//獲取布局器
DTCoreTextLayouter *layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:attributedString];
NSRange entireString = NSMakeRange(0, [attributedString length]);
//獲取Frame
DTCoreTextLayoutFrame *layoutFrame = [layouter layoutFrameWithRect:_viewMaxRect range:entireString];
//得到大小
CGSize sizeNeeded = [layoutFrame frame].size;
return sizeNeeded;
}
1.3響應(yīng)事件
如果我們希望在DTAttributedLabel上操作點(diǎn)擊事件,那么對應(yīng)的我們也應(yīng)該在Html中使用超鏈接A標(biāo)簽。這里我們需要用到如下的代理方法如下:
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView
viewForLink:(NSURL *)url
identifier:(NSString *)identifier
frame:(CGRect)frame{
//此方法需要我們在這里返回一個可以點(diǎn)擊響應(yīng)的控件如Button,然后就可以很方便的處理響應(yīng)事件了,其中:
//url:Html中的超鏈接(可以根據(jù)需要設(shè)置http、tel、mailto等)
//identifier:屬于該視圖的唯一性標(biāo)識
//frame:超鏈接字符串所在的位置,需要將自定義響應(yīng)按鈕設(shè)置為此Frame
return nil;
}
注意:如果我們需要顯示一些輸入框之類的視圖,也可以借助此方法。
最終效果圖:

2.DTAttributedTextView
其實(shí),DTAttributedTextView和DTAttributedLabel的用法幾乎一樣。從其源碼上就能看出,DTAttributedLabel繼承于DTAttributedTextContentView的,而DTAttributedTextView包含DTAttributedTextContentView屬性??梢哉fDTAttributedTextView就是借助DTAttributedLabel實(shí)現(xiàn)的視圖。
DTAttributedTextView是支持滑動顯示的富文本視圖,在使用時計(jì)算富文本的Frame也就沒那么必要了,一般我們都會指定它的Frame。其在這里的用法就不累述了。
3.DTAttributedTextCell
如果我們需要在單元格上顯示富文本,DTCoretText也為我們提供了特有的類來解決這個問題,那就是DTAttributedTextCell。通過這個單元格類,我們可以方便的設(shè)置富文本以及獲取單元格高度。以下是使用DTAttributedTextCell顯示富文本的核心代碼:
3.1. 聲明控制器內(nèi)屬性
@interface TestTableViewController ()<UITableViewDataSource,UITableViewDelegate,DTAttributedTextContentViewDelegate,DTLazyImageViewDelegate>
@property(nonatomic,strong)UITableView *tableView;
//普通單元格與富文本單元格
@property (nonatomic, copy) NSString *cellID_Normal;
@property (nonatomic, copy) NSString *cellID_DTCoreText;
//類似tabelView的緩沖池,用于存放圖片大小
@property (nonatomic, strong) NSCache *imageSizeCache;
@property (nonatomic,strong)NSCache *cellCache;
//表視圖數(shù)據(jù)源
@property (nonatomic, strong) NSArray *dataSource;
//當(dāng)前表視圖是否在滑動
@property (nonatomic,assign)BOOL isScrolling;
@end
3.2.表視圖代理方法返回單元格及其高度
//代理方法:返回單元格
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
switch (indexPath.section) {
case 0:{
//普通單元格
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_cellID_Normal];
cell.textLabel.text = self.dataSource[indexPath.section][indexPath.row];
return cell;
break;
}
case 1:{
//自定義方法,創(chuàng)建富文本類型的單元格
ZSDTCoreTextCell *dtCell = (ZSDTCoreTextCell *) [self tableView:tableView prepareCellForIndexPath:indexPath];
return dtCell;
break;
}
default:
break;
}
return nil;
}
//返回單元格高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(nonnull NSIndexPath *)indexPath{
switch (indexPath.section) {
case 0:{
return 50;
break;
}
case 1:{
//返回富文本類型單元格的方法
ZSDTCoreTextCell *cell = (ZSDTCoreTextCell *)[self tableView:tableView prepareCellForIndexPath:indexPath];
return [cell requiredRowHeightInTableView:tableView];
break;
}
default:
break;
}
return 0;
}
3.3.懶加載處理無寬高屬性的圖片
#pragma mark - DTAttributedTextContentViewDelegate
//對于沒有在Html標(biāo)簽里設(shè)置寬高的圖片,在這里為其設(shè)置占位
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttachment:(DTTextAttachment *)attachment frame:(CGRect)frame{
if([attachment isKindOfClass:[DTImageTextAttachment class]]){
//自定義的ZSDTLazyImageView繼承于DTLazyImageView,增加了一個屬性textContentView
//用于更新圖片大小
ZSDTLazyImageView *imageView = [[ZSDTLazyImageView alloc] initWithFrame:frame];
imageView.delegate = self;
imageView.image = [(DTImageTextAttachment *)attachment image];
imageView.textContentView = attributedTextContentView;
imageView.url = attachment.contentURL;
return imageView;
}
return nil;
}
//對于無寬高的圖片懶加載,緩存記錄其大小,然后執(zhí)行表視圖更新
- (void)lazyImageView:(ZSDTLazyImageView *)lazyImageView didChangeImageSize:(CGSize)size{
BOOL needUpdate = NO;
NSURL *url = lazyImageView.url;
NSPredicate *pred = [NSPredicate predicateWithFormat:@"contentURL == %@", url];
/* update all attachments that matchin this URL (possibly multiple
images with same size)
*/
for (DTTextAttachment *oneAttachment in [lazyImageView.textContentView.layoutFrame textAttachmentsWithPredicate:pred]){
// update attachments that have no original size, that also sets the display size
if (CGSizeEqualToSize(oneAttachment.originalSize, CGSizeZero)){
oneAttachment.originalSize = size;
NSValue *sizeValue = [_imageSizeCache objectForKey:oneAttachment.contentURL];
if (!sizeValue) {
//將圖片大小記錄在緩存中,但是這種圖片的原始尺寸可能很大,所以這里設(shè)置圖片的最大寬
//并且計(jì)算高
CGFloat aspectRatio = size.height / size.width;
CGFloat width = ZSToolScreenWidth - 15*2;
CGFloat height = width * aspectRatio;
CGSize newSize = CGSizeMake(width, height);
[_imageSizeCache setObject:[NSValue valueWithCGSize:newSize]forKey:url];
}
needUpdate = YES;
}
}
if (needUpdate){
//有新的圖片尺寸被緩存記錄的時候,需要刷新表視圖
[self reloadCurrentCell];
}
}
3.4.創(chuàng)建富文本單元格的方法
#pragma mark - private Methods
//創(chuàng)建富文本單元格,并更新單元格上的數(shù)據(jù)
//ZSDTCoreTextCell是自定義的繼承于DTCoreTextCell的單元格
- (ZSDTCoreTextCell *)tableView:(UITableView *)tableView prepareCellForIndexPath:(NSIndexPath *)indexPath{
NSString *key = [NSString stringWithFormat:@"dtCoreTextCellKEY%ld-%ld", (long)indexPath.section, (long)indexPath.row];
ZSDTCoreTextCell *cell = [_cellCache objectForKey:key];
if (!cell){
cell = [[ZSDTCoreTextCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_cellID_DTCoreText];
cell.attributedTextContextView.edgeInsets = UIEdgeInsetsMake(0, 15, 0, 15);
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
cell.accessoryType = UITableViewCellAccessoryNone;
cell.hasFixedRowHeight = NO;
cell.textDelegate = self;
cell.attributedTextContextView.shouldDrawImages = YES;
//記錄在緩存中
[_cellCache setObject:cell forKey:key];
}
//2.設(shè)置數(shù)據(jù)
//2.1為富文本單元格設(shè)置Html數(shù)據(jù)
[cell setHTMLString:self.dataSource[indexPath.section][indexPath.row]];
//2.2為每個占位圖(圖片)設(shè)置大小,并更新
for (DTTextAttachment *oneAttachment in cell.attributedTextContextView.layoutFrame.textAttachments) {
NSValue *sizeValue = [_imageSizeCache objectForKey:oneAttachment.contentURL];
if (sizeValue) {
cell.attributedTextContextView.layouter=nil;
oneAttachment.displaySize = [sizeValue CGSizeValue];
[cell.attributedTextContextView relayoutText];
}
}
[cell.attributedTextContextView relayoutText];
return cell;
}
3.5.處理表視圖的刷新
如果當(dāng)前表視圖在滑動就不執(zhí)行刷新,因?yàn)榛瑒訒r候會自動調(diào)用表視圖的刷新方法
- (void)reloadCurrentCell{
if (self.isScrolling) {
return;
}
//如果當(dāng)前表視圖沒有在滑動,就手動刷新當(dāng)前在屏幕顯示的單元格
NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
if(indexPaths){
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
});
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
_isScrolling = NO;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
_isScrolling = YES;
}
3.6.set方法創(chuàng)建表視圖與數(shù)據(jù)源
#pragma mark - set/get方法
- (UITableView *)tableView{
if (_tableView == nil) {
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, ZSToolScreenWidth, ZSToolScreenHeight-64) style:UITableViewStylePlain];
_tableView.dataSource = self;
_tableView.delegate = self;
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:_cellID_Normal];
//[_tableView registerClass:[ZSDTCoreTextCell class] forCellReuseIdentifier:_cellID_DTCoreText];
}
return _tableView;
}
- (NSArray *)dataSource{
if(_dataSource == nil){
NSMutableArray *noramDataArray = @[].mutableCopy;
NSMutableArray *htmlDataArray = @[].mutableCopy;
NSArray *images = @[@"https://i0.hdslb.com/bfs/archive/d5ad3cf95d32f3d2f2e3471a39120237200d84d8.jpg",
@"https://i0.hdslb.com/bfs/archive/71d2fed927d9351e759f408ca7d66c556c37a6b4.jpg",
@"https://i0.hdslb.com/bfs/archive/7f520b31b67cd5d89dd30b61b40711327bb00288.png",
@"https://i0.hdslb.com/bfs/archive/6edbe81bf74c106087ad139aca169d6e8d9d963b.jpg",
@"https://i0.hdslb.com/bfs/archive/805aa8f7ae722fcc277f425bb9927e29ec1d2468.jpg",
@"https://i0.hdslb.com/bfs/archive/a7c61d94c583363a970d2a2e339eea97f8f32317.jpg",
@"https://i0.hdslb.com/bfs/archive/b447ee1fd63b4cf6f4465a5621cc12898867d26b.jpg"];
for(int i = 0;i<1000;i++){
if(i <6){
[noramDataArray addObject:[NSString stringWithFormat:@"測試普通單元格:%d",i]];
}
//這里提供的Html圖片鏈接,沒有寬高屬性,代碼中已經(jīng)演示了如何處理
int k = i % 6;
NSString *htmlString =[NSString stringWithFormat:@"<span style=\"color:#333;font-size:15px;\"><strong>測試富文本單元格%d:</strong></span><br/><span style=\"color:#333;font-size:15px;\">記??!砍價(jià)是由你自己先砍,砍不動時再由砍價(jià)師繼續(xù)砍;由砍價(jià)師多砍下的部分,才按照下列標(biāo)準(zhǔn)收費(fèi):</span><br/><span style=\"color:#333;font-size:15px;\"><img src=%@ _src=%@></span>",i,images[k],images[k]];
[htmlDataArray addObject:htmlString];
}
_dataSource = @[noramDataArray,htmlDataArray];
}
return _dataSource;
}
使用上述方法使用DTAttributedTextCell的效果圖如下:

四、可能遇到的錯誤
原本上,在項(xiàng)目中使用只需將Release-iphones的DTCoreText.framework和DTFoundation.framework中拖入工程就可以使用了。然而我們編譯工程卻會遇到這樣一個錯誤:"image not found”
解決方法如下圖:

五、參考鏈接
1.DTCoreText集成:
http://m.itdecent.cn/p/b09d3fa1cd98
2.DTCoreText使用總結(jié):
http://duxinfeng.com/2017/08/02/DTCoreText%E4%BD%BF%E7%94%A8%E6%80%BB%E7%BB%93/
3.iOS富文本框架DTCoreText在UITableView上的使用
http://blog.csdn.net/lala2231/article/details/50780842
4.CoretText的優(yōu)缺點(diǎn)
http://m.itdecent.cn/p/6f8162537975