说说和聊天界面,会要求把链接,@,##等,特殊的字符串能点击,可能它涉及到CoreText层,自己写起来比较麻烦,平常忙着做项目,也没时间去研究它,都用的第三方,最近项目不紧,自己研究了一下。写篇文章,加深一下印象

这个工程主要创建4个类,MainViewController类属于c层,MainCell和RichLabel属于view层,RichLink属于model层,这四个类中MainViewController,MainCell,RichLink这三个不用看,我也没注释,就是一些简单的创建UILabel控件的代码,主要看RichLabel

下面是MainViewController.m里面的代码,.h没代码就不写出来了

#import "MainViewController.h"
#import "MainCell.h"
@interface MainViewController ()
/** 注释 */
@property (nonatomic,strong) NSArray *dataArr;
@end
@implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];


_dataArr = @[@"【国防部:中国首艘国产航母舾装工作进展顺利】中国国防部31号表示,中国首艘航母进展顺利,目前正进行系统设备调试和舾装施工,并将全面开展系泊试验。系泊试验后还将进行海上试验,以及交付部队阶段,相信整个后续工作,大概需时至少两年。 Via凤凰早班车",

@"特朗普将捐100万美元帮助飓风灾民渡过难关 2日再访灾区】美国白宫发言人桑德斯8月31日透露,总统特朗普将以个人名义捐出100万美元,帮助受飓风“哈维”影响的得克萨斯州和路易斯安那州灾民救灾。此外,特朗普夫妇还将于9月2日再次前往受灾地区视察。http://t.cn/RNfnGvO",

@"建筑工人在商场弹起优美钢琴曲 网友:隐士高人 铁汉柔情】“可能为了生活,需要放下梦想”,30日,香港曹先生在商场里拍下这样一幕:一位身着工地制服的建筑工在认真弹奏钢琴。许多网友被这美妙的音乐和投入的弹奏所感动~@新京报 http://t.cn/RN5w8vc",

@"【出租车驾驶员从业资格考试将公开题库 两考合一】交通运输部将对出租汽车驾驶员资格考试进行改革。交通运输部运输服务司副司长王绣春表示,要对出租车驾驶员从业资格考试进一步优化完善,将实行全国公共科目考试和区域科目考试“两考合一”,优化题库试题、公开考试题库,强化便民利民服务。 ...全文: http://m.weibo.cn/1730243272/4147093503116821",

@"【“十一”黄金周国内游将达6.5亿人次 又能用“朋友圈”环游世界了呢[doge]】《2017十一黄金周旅游趋势预测报告》称,今年“十一”将有6.5亿人次国内游、超600万人出境游。专家建议,尽量错峰出游。看“朋友圈”环游世界的时候又到了...http://t.cn/RNGelZ2 “十一”你选择在家还是出去玩?",

@"#助威国足战在一起# 【郜林点球破门 中国1-0乌兹别克斯坦】中国队1:0战胜乌兹别克斯坦,武汉体育中心五万人赛后大合唱《怒放的生命》,教练@米卢、球员@郜林 等纷纷微博发声,祝贺国足,中国赢下去! http://t.cn/RNqBrpG",

@"#荐书#今日推荐:《理解人性》。对人类来说,最难的是认识自己和改变自己。在书中,作者运用个性心理学的原理,对人的性格进行了科学的剖析,着重强调人的社会性和社会感,强调个人的人生观和价值观在形成性格的过程中所起的作用。 http://t.cn/RNqQjdK"];

}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataArr.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return [MainCell getCellHeight:_dataArr[indexPath.row]];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = @"MainCell";
MainCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[MainCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
cell.content = _dataArr[indexPath.row];
return cell;
}
@end
下面是cell.h文件里面的代码
#import<UIKit/UIKit.h>
@class RichLabel;
@interface MainCell : UITableViewCell
@property (strong, nonatomic) RichLabel *contentLb;
@property (nonatomic, copy)NSString *content;
+ (CGFloat)getCellHeight:(NSString *)content;
@end
下面是cell.m文件里面的代码
#import "MainCell.h"
#import "RichLabel.h"
#define kEdge 15
@implementation MainCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.contentLb = [[RichLabel alloc] initWithFrame:CGRectZero];
self.contentLb.userInteractionEnabled = YES;
self.contentLb.numberOfLines = 0;
self.contentLb.lineBreakMode = NSLineBreakByWordWrapping;
self.contentLb.font = kFontSize;
[self.contentView addSubview:self.contentLb];
}
return self;
}
+(CGSize)sizeOfmyText:(NSString *)text font:(UIFont*)myfont width:(CGFloat)mywidth lineSpacing:(CGFloat)lineSpacing
{
if([text isEqual:[NSNull null]] || text==nil || [text isEqualToString:@""])
return CGSizeMake(0.0f, 0.0f);
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc]init];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
if (lineSpacing > 0) {
paragraphStyle.lineSpacing = lineSpacing;
}
NSDictionary *attributes = @{NSFontAttributeName:myfont, NSParagraphStyleAttributeName:paragraphStyle.copy};
CGSize labelsize = [text boundingRectWithSize:CGSizeMake(mywidth, 2000) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size;
return labelsize;
}
- (void)setContent:(NSString *)content{
_content = content;
self.contentLb.content = content;
CGFloat w = self.contentView.frame.size.width - 2*kEdge;
self.contentLb.frame = CGRectMake(kEdge, 0, w, [MainCell sizeOfmyText:content font:kFontSize width:w lineSpacing:0].height);
}
+ (CGFloat)getCellHeight:(NSString *)content{
return [MainCell sizeOfmyText:content font:kFontSize width:[UIScreen mainScreen].bounds.size.width - 2*kEdge lineSpacing:0].height;
}
@end
下面是RichLink.h的代码,.m没代码,就不写出来了
#import<Foundation/Foundation.h>
@interface RichLink : NSObject
@property (nonatomic, copy) NSString *linkStr;
@property (nonatomic, assign) NSRange range;
@end
RichLabel.h代码,没什么好解释的
#import <UIKit/UIKit.h>
#define kFontSize [UIFont systemFontOfSize:14]
@interface RichLabel : UILabel
@property (nonatomic, copy)NSString *content;
@end
上面的不用看,上面写那么多,就为了方便你粘贴复制到你工程,,这才本文重点,RichLabel.m代码
#import "RichLabel.h"
#import <CoreText/CoreText.h>
#import "RichLink.h"
@implementation RichLabel
- (void)setContent:(NSString *)content{    
_content = content;    
self.attributedText = [self matchesAttributedString:content];
}
#pragma mark 处理链接颜色
- (NSAttributedString *)matchesAttributedString:(NSString *)str{    
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:str];    
for (NSTextCheckingResult *match in self.matchesArr)    {        
[attrStr addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:match.range];    
}        
[attrStr addAttribute:NSFontAttributeName value:kFontSize range:NSMakeRange(0, attrStr.length)];    
return attrStr;
}
//通过正则表达式匹配出来的特殊文本放到一个数组
- (NSArray *)matchesArr{    
NSError *error;    
NSString *urlPattern = @"((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)";//匹配url   
 NSString *emotionPattern = @"\\[[0-9a-zA-Z\\u4e00-\\u9fa5]+\\]";//匹配自定义表情 带[]    
NSString *atPattern = @"@[0-9a-zA-Z\\u4e00-\\u9fa5]+";//匹配@    NSString *topicPattern = @"#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";//匹配##   
 NSString *pattern = [NSString
stringWithFormat:@"%@|%@|%@|%@",urlPattern,emotionPattern,atPattern,topicPattern];        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern                                                                          options:NSRegularExpressionCaseInsensitive                                                                            error:&error];    NSArray *arrayOfAllMatches = [regex matchesInString:_content options:0 range:NSMakeRange(0, [_content length])];    
return arrayOfAllMatches;
}
#pragma mark 点击事件
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event
{
UITouch *touch = touches.allObjects[0];
CGPoint touchPos = [touch locationInView:self];
//    __weak typeof (HWStatusTextLabel *)weakSelf = self;
[self getTapFrameWithTouchPoint:touchPos result:^(NSString *string, NSRange range, NSInteger index) {
NSLog(@"string : %@ NSStringFromRange : %@ index : %ld",string,NSStringFromRange(range),(long)index);
}];
}
#pragma mark - getTapFrame
- (BOOL)getTapFrameWithTouchPoint:(CGPoint)point result:(void(^) (NSString *string,NSRange range , NSInteger index))resultBlock{
//判断点击的区域是否超出label的范围
CGFloat verticalOffset = 0.0f;
if (!CGRectContainsPoint(CGRectInset(self.bounds, 0, -verticalOffset),point)) return NO;
//这两句 创建了一个路径,用于表示文本在此区域内绘制着。Mac下Core Text支持在各种形状中绘制文本,比如矩形和椭圆。但iOS下仅支持矩形。在本例中,使用整个视图作为文本绘制的路径,直接将self.bounds转变为一个CGPath
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, CGRectGetWidth(self.bounds), self.bounds.size.height));
//CTFramesetter是绘制Core Text文本的最重要的一个类。用它来管理所有的字体引用和文本绘制块。现在你需要知道的仅仅是使用 CTFramesetterCreateWithAttributedString函数创建一个 CTFramesetter。在创建出CTFramesetter之后,我们可以用 CTFramesetterCreateFrame 得到文本一个渲染范围,以及绘制文本时文本显示范围,方法最终返回一个文本绘制块
//我们不能直接拿CGRect 来得到文本的范围,因为用CGRect得到的范围,和真正文本显示的范围,还是有偏差的
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attributedText);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
//得到绘制的这块文本中有多少行字符串,放到类型为CFArrayRef里,用处和我们的数组很像
CFArrayRef lines = CTFrameGetLines(frame);
if (!lines) {//如果没有直接返回
return NO;
}
//得到行数
CFIndex count = CFArrayGetCount(lines);
//定一个origins数组,里面存放CGPoint的元素,开辟count个空间
CGPoint origins[count];
//得到每一行的坐标原点存放到origins里面
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
//因为在UIKit中坐标系原点在左上方,CoreText中在左下方,左下方的
CGAffineTransform transform = [self transformForCoreText];
//循环每一行
for (CFIndex i = 0; i < count; i++) {
//得到每一行的原点
CGPoint linePoint = origins[i];
//得到每一行
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//这个会涉及到字形的问题,这有个链接,可以看一下,blog.csdn.net/fengsh998/article/details/8701738
//得到每行的frame
CGRect flippedRect = [self getLineBounds:line point:linePoint];
//坐标转换后得到的frame
CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
rect = CGRectInset(rect, 0, 0);
//在一行文本的范围内,y轴向内偏移verticalOffset距离
rect = CGRectOffset(rect, 0, -verticalOffset);
//判断点击的点是否在文本范围内,如果在,证明点击的是这一行
if (CGRectContainsPoint(rect, point)) {
//因为每行算frame高度的时候,没有加上行距,所以每行的高度,要比总体的高度除以行数,算出来的要少。所以这里要重新算点击的ponit,求出相对于点击当前行原点的point,防止点到每两行之间的空白处,也相应点击事件
CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));
//获取在当前行的下标
CFIndex index = CTLineGetStringIndexForPosition(line, relativePoint);
//链接个数
NSInteger link_cout = self.linkArr.count;
for (int j = 0; j < link_cout; j++) {
RichLink *link = self.linkArr[j];
//每个链接的范围
NSRange link_range = link.range;
//判断下标是否在连接的范围,如果是,证明点击的该链接
if (NSLocationInRange(index, link_range)) {
if (resultBlock) {
resultBlock (link.linkStr,link.range,(NSInteger)j);
}
return YES;
}
}
}
}
return NO;
}
- (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point
{
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat leading = 0.0f;
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;//这个地方我们没有把行距加上,防止点到空白处,也能触发点击事件
//这个point.y - ascent需要说一下,看我上面发的链接的博客,我们知道,她的pont.y在基线的左侧,上行高度ascent从原点到字体中最高的字形的顶部的距离
return CGRectMake(point.x, point.y - ascent, width, height);
}
//先翻转,再向下平移self.bounds.size.height,本来原点在左下方,现在在左上方
- (CGAffineTransform)transformForCoreText
{
return CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.0f, -1.0f);
}
//对匹配出来的特殊文本进行model封装,放到linkArr数组,RichLink存放的有该文本字符串和范围
- (NSArray *)linkArr
{
NSMutableArray *linkArr = [NSMutableArray arrayWithCapacity:1];
for (NSTextCheckingResult *match in self.matchesArr)
{
NSString *substringForMatch = [self.text substringWithRange:match.range];
NSRange range = [self.text rangeOfString:substringForMatch];
RichLink *link = [RichLink new];
link.linkStr = substringForMatch;
link.range = range;
[linkArr addObject:link];
}
return linkArr;
}
@end