有时候由于后台内容是由编辑器编辑的,APP 调取接口返回的是 HTML 格式的字符串。虽然 iOS 的 NSAttributeString 本身自带 HTML 字符串解析功能,但是如果想实现下面的效果就有点力不从心了。
上图效果可以支持大部分字体样式展示,支持点击文本实现展开关闭,支持超链接和图片的点击回调,并且在文本收起时或者图片未加载完成时不展示图片。附上Demo链接:https://github.com/xuqisheng1/HTMLAnalysisDemo
1、简单的 HTML 解析:
对于简单的 HTML 文本展示,我们可以借助富文本 NSAttributeString 进行解析,该方法可以满足大部分的 HTML 文本解析需求:
NSString *HTMLStr = @"<p>这是一个测试跳转链接<a href=\"https://www.baidu.com\" target=\"_blank\" rel=\"noopener\">超链接</a>,可以点击跳转。其他标签也可以,主要是带有交互事件的标签需要我们自己解析,采用原生的交互方法</p> <p>这是另外一段文字,<strong>粗体字</strong>测试,中文不支持<i>斜体字</i>,英文可以 <i>abcde</i></p>"
NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc]initWithData:[HTMLStr dataUsingEncoding:NSUnicodeStringEncoding] options:@{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType} documentAttributes:nil error:nil];
2、复杂并且带交互的 HTML 解析:
由于 HTML 标签是有一定规范的,我原先是想通过正则匹配解析出标签,转成字符串数组后再拼接起来加上交互,后来发现该方法工作量巨大,且对于复杂嵌套的标签难以保证解析结果的准确性。
既然 NSAttributeString 本身自带解析 HTML 功能,我们可以借助它来实现 HTML 到富文本的转换,NSAttributeString 还提供了这么一个实例方法:
- (void)enumerateAttributesInRange:(NSRange)enumerationRange options:(NSAttributedStringEnumerationOptions)opts usingBlock:(void (NS_NOESCAPE ^)(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange range, BOOL *stop))block ;
通过该方法我们我们可以拿到超链接和图片等我们想要加上交互富文本的 Range,将枚举出来的富文本转换成我们自定义的模型,放在一个数组里,此过程可以同时调整字体大小样式为手机端想要的实际效果。这里还需要借助另外三个第三方库帮助我们解析:YYText(富文本的样式定义等功能)、TFHpple(解析出所有的图片资源路径)、SDWebImage(图片的异步加载和缓存),可以通过CocoaPods导入。
自定义模型 AttributedStrModel.h :
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef enum {
ImageAttributedStrType = 0,
TextAttributedStrType,
}AttributedStrType;
@interface AttributedStrModel : NSObject
@property (nonatomic, assign)AttributedStrType type;
@property (nonatomic, strong)NSMutableAttributedString *attributeStr; //文本内容
@property (nonatomic, copy)NSString *link; //超链接
@property (nonatomic, copy)NSString *imgName; //图片名称,即图片URL路径后面带的图片名称
@property (nonatomic, strong)UIImage *image; //加载完成的图片对象
@end
内容解析 HTMLAnalysisHelper.h :
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#define kScreen_WIDTH [UIScreen mainScreen].bounds.size.width
#define kDefaultFontSize 16.0
#define kDefaultImgWidth kScreen_WIDTH
#define kDefaultTextLineSpacing 2.0
#define kDefaultParagraphSpacing (kDefaultTextLineSpacing * 2)
@interface HTMLAnalysisHelper : NSObject
@property (nonatomic, assign)CGFloat fontSize; //统一的字体大小
@property (nonatomic, assign)CGFloat imageWidth; //图片宽度
@property (nonatomic, assign)CGFloat textLineSpacing; //行间距
@property (nonatomic, assign)CGFloat paragraphSpacing; //段间距
@property (nonatomic, strong)NSMutableAttributedString *closeStr; //
@property (nonatomic, strong)NSMutableAttributedString *openStr; //
- (void)analysisWithHTMLStr:(NSString *)htmlStr;
- (void)getImgUrlArrWithHTMLStr:(NSString *)htmlStr;
@property (nonatomic, copy)void(^linkClickedBlock)(NSString *linkStr); //点击超链接
@property (nonatomic, copy)void(^openCloseBlock)(void); //展开关闭
@property (nonatomic, copy)void(^imageClickedBlock)(UIImage *image); //点击图片
@end
内容解析 HTMLAnalysisHelper.m :
#import "HTMLAnalysisHelper.h"
#import "AttributedStrModel.h"
#import <YYText.h>
#import <TFHpple/TFHpple.h>
#import <UIImageView+WebCache.h>
@interface HTMLAnalysisHelper ()
@property (nonatomic, strong)NSMutableArray *attributeStrArr;
@property (nonatomic, strong)NSMutableArray *imgUrlStrArr; //html中所嵌的图片url 数组
@end
@implementation HTMLAnalysisHelper
- (instancetype)init
{
if (self = [super init]) {
self.fontSize = kDefaultFontSize;
self.imageWidth = kDefaultImgWidth;
self.textLineSpacing = kDefaultTextLineSpacing;
self.paragraphSpacing = kDefaultParagraphSpacing;
}
return self;
}
- (void)setFontSize:(CGFloat)fontSize
{
_fontSize = fontSize;
[self getAttributeStrWithOpenState:YES];
[self getAttributeStrWithOpenState:NO];
}
- (void)setImageWidth:(CGFloat)imageWidth
{
_imageWidth = imageWidth;
[self getAttributeStrWithOpenState:YES];
[self getAttributeStrWithOpenState:NO];
}
- (void)setTextLineSpacing:(CGFloat)textLineSpacing
{
_textLineSpacing = textLineSpacing;
[self getAttributeStrWithOpenState:YES];
[self getAttributeStrWithOpenState:NO];
}
- (void)setParagraphSpacing:(CGFloat)paragraphSpacing
{
_paragraphSpacing = paragraphSpacing;
[self getAttributeStrWithOpenState:YES];
[self getAttributeStrWithOpenState:NO];
}
#pragma mark -- 解析字符串,放到一个数组里
- (void)analysisWithHTMLStr:(NSString *)htmlStr
{
//先转成富文本
NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc]initWithData:[htmlStr dataUsingEncoding:NSUnicodeStringEncoding] options:@{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType} documentAttributes:nil error:nil];
//枚举取出所有富文本,放到一个AttributedStrModel数组里
[self.attributeStrArr removeAllObjects];
__weak typeof(self) wself = self;
[attributeString enumerateAttributesInRange:NSMakeRange(0, attributeString.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
// NSLog(@"%@",attrs);
//创建模型装载数据对象
AttributedStrModel *attributeStrModel = [[AttributedStrModel alloc]init];
NSTextAttachment *attachment = [attrs objectForKey:NSAttachmentAttributeName];
if (attachment) { //图片
attributeStrModel.type = ImageAttributedStrType;
// attachment.fileWrapper.preferredFilename 拿到的是URL路径最后面的图片名称
attributeStrModel.imgName = [attachment.fileWrapper.preferredFilename componentsSeparatedByString:@"."].firstObject;
}
else{ // 文本
attributeStrModel.type = TextAttributedStrType;
//调整字体大小为我们想要的大小
attributeStrModel.attributeStr = [[NSMutableAttributedString alloc]initWithAttributedString:[attributeString attributedSubstringFromRange:range]];
UIFont *font = [attrs objectForKey:NSFontAttributeName];
if (!font) {
font = [UIFont systemFontOfSize:self.fontSize];
}
else{
font = [UIFont fontWithName:font.fontName size:self.fontSize];
}
attributeStrModel.attributeStr.yy_font = font;
attributeStrModel.attributeStr.yy_color = [UIColor blackColor];
//去掉超链接的下划线
NSURL *link = [attrs objectForKey:NSLinkAttributeName];
if (link) {
attributeStrModel.attributeStr.yy_underlineStyle = NSUnderlineStyleNone;
}
}
[wself.attributeStrArr addObject:attributeStrModel];
}];
[self getAttributeStrWithOpenState:YES];
[self getAttributeStrWithOpenState:NO];
[self getImgUrlArrWithHTMLStr:htmlStr];
}
#pragma mark -- 获取所有的图片URL并加载
- (void)getImgUrlArrWithHTMLStr:(NSString *)htmlStr
{
[self.imgUrlStrArr removeAllObjects];
NSData *data = [htmlStr dataUsingEncoding:NSUTF8StringEncoding];
TFHpple *xpathParser = [[TFHpple alloc]initWithHTMLData:data];
// 获取所有的图片链接
NSArray *elements = [xpathParser searchWithXPathQuery:@"//img"];
for (TFHppleElement *element in elements) {
if (element.attributes[@"src"]) {
[self.imgUrlStrArr addObject:element.attributes[@"src"]];
}
}
//加载图片
__weak typeof(self) wself = self;
for (NSString *imgUrlStr in self.imgUrlStrArr) {
//取出URL路径最后面的图片名称
NSString *imgName = [[[imgUrlStr componentsSeparatedByString:@"?"] firstObject] lastPathComponent];
imgName = [imgName componentsSeparatedByString:@"."].firstObject;
NSURL *imgUrl = [NSURL URLWithString:imgUrlStr];
SDWebImageManager *manager = [SDWebImageManager sharedManager] ;
[manager downloadImageWithURL:imgUrl options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
for (AttributedStrModel *strModel in wself.attributeStrArr) {
if (strModel.type == ImageAttributedStrType) {
//比对图片名称是否一致
if ([strModel.imgName isEqualToString:imgName]) {
strModel.image = image;
[wself getAttributeStrWithOpenState:YES];
break;
}
}
}
}
}];
}
}
#pragma mark -- 获取展开和关闭的富文本内容
- (void)getAttributeStrWithOpenState:(BOOL)isOpen
{
if (!self.attributeStrArr.count) {
return;
}
//拼接要显示的字符串
NSMutableAttributedString *attributeStrM = [[NSMutableAttributedString alloc]init];
for (AttributedStrModel *strModel in self.attributeStrArr) {
//收起状态下只拼接文本
if (strModel.type == TextAttributedStrType) {
[attributeStrM appendAttributedString:strModel.attributeStr];
}
//展开状态下且图片已加载完成则拼接上图片
else if (strModel.type == ImageAttributedStrType && strModel.image && isOpen){
//等比缩放
CGFloat imageW = self.imageWidth;
CGFloat imageH = self.imageWidth / strModel.image.size.width * strModel.image.size.height;
NSMutableAttributedString *attachText = [NSMutableAttributedString yy_attachmentStringWithContent:strModel.image contentMode:UIViewContentModeScaleAspectFill attachmentSize:CGSizeMake(imageW, imageH) alignToFont:[UIFont systemFontOfSize:self.fontSize] alignment:YYTextVerticalAlignmentCenter];
[attributeStrM appendAttributedString:attachText];
}
}
attributeStrM.yy_lineSpacing = self.textLineSpacing;
attributeStrM.yy_paragraphSpacing = self.paragraphSpacing;
//添加点击 所有内容 实现展开关闭事件
__weak typeof(self) wself = self;
[attributeStrM yy_setTextHighlightRange:NSMakeRange(0, attributeStrM.length) color:[UIColor blackColor] backgroundColor:nil tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
if (wself.openCloseBlock) {
wself.openCloseBlock();
}
}];
//添加点击 超链接 & 点击图片 的回调
[attributeStrM enumerateAttributesInRange:NSMakeRange(0, attributeStrM.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
NSURL *link = [attrs objectForKey:NSLinkAttributeName];
//有超链接
if (link) {
[attributeStrM yy_setTextHighlightRange:range color:[UIColor blueColor] backgroundColor:[UIColor lightGrayColor] tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
if (wself.linkClickedBlock) {
wself.linkClickedBlock(link.absoluteString);
}
}];
}
YYTextAttachment *attachment = [attrs objectForKey:YYTextAttachmentAttributeName];
//图片不为空
if (attachment) {
[attributeStrM yy_setTextHighlightRange:range color:nil backgroundColor:nil tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
if (wself.imageClickedBlock) {
wself.imageClickedBlock(attachment.content);
}
}];
}
}];
if (isOpen) {
self.openStr = attributeStrM;
}
else{
self.closeStr = attributeStrM;
}
}
#pragma mark -- LazyLoad
- (NSMutableArray *)attributeStrArr
{
if (!_attributeStrArr) {
_attributeStrArr = [[NSMutableArray alloc]init];
}
return _attributeStrArr;
}
- (NSMutableArray *)imgUrlStrArr
{
if (!_imgUrlStrArr) {
_imgUrlStrArr = [[NSMutableArray alloc]init];
}
return _imgUrlStrArr;
}
@end
测试最终效果:
#import "ViewController.h"
#import <YYText.h>
#import "HTMLAnalysisHelper/HTMLAnalysisHelper.h"
#define kCloseLineNum 3
#define kContentFontSize 16.0
#define kContentFont [UIFont systemFontOfSize:kContentFontSize]
@interface ViewController ()
@property (nonatomic, strong)YYLabel *contentLbl;
@property (nonatomic, copy)NSString *HTMLStr;
@property (nonatomic, strong)HTMLAnalysisHelper *helper;
@property (nonatomic, assign)BOOL openState;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self buildView];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)buildView
{
[self.view addSubview:self.contentLbl];
self.contentLbl.frame = CGRectMake(0, 20, kScreen_WIDTH, 0);
//解析HTML字符串
[self.helper analysisWithHTMLStr:self.HTMLStr];
self.contentLbl.attributedText = self.helper.closeStr;
//调整高度
CGRect frame = self.contentLbl.frame;
frame.size.height = [self getYYLabelHeight:self.contentLbl];
self.contentLbl.frame = frame;
}
/**
* 根据内容高度获取YYLabel标签的高度
*/
- (CGFloat)getYYLabelHeight:(YYLabel *)label
{
NSMutableAttributedString *innerText = [label valueForKey:@"_innerText"];
YYTextContainer *innerContainer = [label valueForKey:@"_innerContainer"];
YYTextContainer *container = [innerContainer copy];
container.size = CGSizeMake(label.frame.size.width, CGFLOAT_MAX);
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:innerText];
return layout.textBoundingSize.height;
}
- (void)changeOpenState
{
self.openState = !self.openState;
self.contentLbl.attributedText = self.openState?self.helper.openStr:self.helper.closeStr;
self.contentLbl.numberOfLines = self.openState?0:kCloseLineNum;
//调整高度
CGRect frame = self.contentLbl.frame;
frame.size.height = [self getYYLabelHeight:self.contentLbl];
self.contentLbl.frame = frame;
}
#pragma mark -- LazyLoad
- (YYLabel *)contentLbl
{
if (!_contentLbl) {
_contentLbl = [[YYLabel alloc] init];
_contentLbl.backgroundColor = [UIColor yellowColor];
_contentLbl.textVerticalAlignment = YYTextVerticalAlignmentTop;
_contentLbl.numberOfLines = kCloseLineNum;
_contentLbl.font = kContentFont;
[self addSeeMoreButton];
}
return _contentLbl;
}
#pragma mark -- 添加全文展开按钮
- (void)addSeeMoreButton
{
NSString *allStr = @"...全文";
NSMutableAttributedString *truncationStr = [[NSMutableAttributedString alloc] initWithString:allStr];
__weak typeof(self) wself = self;
[truncationStr yy_setTextHighlightRange:[truncationStr.string rangeOfString:@"全文"] color:[UIColor blueColor] backgroundColor:nil tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
[wself changeOpenState];
}];
truncationStr.yy_font = kContentFont;
YYLabel *seeMore = [[YYLabel alloc]init];
seeMore.attributedText = truncationStr;
[seeMore sizeToFit];
NSAttributedString *truncationToken = [NSAttributedString yy_attachmentStringWithContent:seeMore contentMode:UIViewContentModeCenter attachmentSize:seeMore.frame.size alignToFont:truncationStr.yy_font alignment:YYTextVerticalAlignmentCenter];
_contentLbl.truncationToken = truncationToken;
}
- (NSString *)HTMLStr
{
if (!_HTMLStr) {
_HTMLStr = @"<p>这是一个测试跳转链接<a href=\"https://www.baidu.com\" target=\"_blank\" rel=\"noopener\">超链接</a>,可以点击跳转。其他标签也可以,主要是带有交互事件的标签需要我们自己解析,采用原生的交互方法</p> <p><img class=\" wscnph\" src=\"http://www.qqma.com/imgpic2/cpimagenew/2018/4/5/c89de4fadcf34dd58bbe789d00a58824.jpg\" data-wscntype=\"image\" data-wscnh=\"636\" data-wscnw=\"823\" /></p> <p>这是另外一段文字,<strong>粗体字</strong>测试,中文不支持<i>斜体字</i>,英文可以 <i>abcde</i></p>";
}
return _HTMLStr;
}
- (HTMLAnalysisHelper *)helper
{
if (!_helper) {
_helper = [[HTMLAnalysisHelper alloc]init];
__weak typeof(self) wself = self;
_helper.openCloseBlock = ^{
[wself changeOpenState];
};
_helper.linkClickedBlock = ^(NSString * _Nonnull linkStr) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:linkStr]];
};
_helper.imageClickedBlock = ^(UIImage * _Nonnull image) {
NSLog(@"点击了图片:%@",image);
};
}
return _helper;
}
@end