iOS 给UILabel添加点击事件

级别:★☆☆☆☆
标签:「UILabel」「TTTAttributedLabel 基本使用」「TTTAttributedLabel 实现」
作者: WYW
审校: QiShare团队


前言:笔者最近需要实现给 UILabel 中的链接添加点击事件的功能。使用 so.com 查了下,发现 TTTAttributedLabel 的封装程度比较好。整理了 TTTAttributedLabel 的基本使用,及部分实现。

TTTAttributedLabel 的基本使用

TTTAttributedLabel.hTTTAttributedLabel.m 放到项目中

遵守 TTTAttributedLabelDelegate 协议

// 遵守TTTAttributedLabelDelegate协议
@interface ViewController () <TTTAttributedLabelDelegate>

创建 TTTAttributedLabel 实例及相应配置

创建 TTTAttributedLabel 实例,添加相应配置。

- (void)setupTTTAttributedLabel {
    
    TTTAttributedLabel *attriLabel = [[TTTAttributedLabel alloc] initWithFrame:CGRectZero];
    attriLabel.font = [UIFont systemFontOfSize:32.0];
    attriLabel.numberOfLines = 0;
    // Automatically detect links when the label text is subsequently changed
    attriLabel.enabledTextCheckingTypes = NSTextCheckingTypeLink;
    // Delegate methods are called when the user taps on a link (see `TTTAttributedLabelDelegate` protocol)
    attriLabel.delegate = self;
    // Repository URL will be automatically detected and linked
    attriLabel.text = @"Fork me on GitHub! (https://github.com/mattt/TTTAttributedLabel/)";
    NSRange range = [attriLabel.text rangeOfString:@"me"];
    // Embedding a custom link in a substring
    [attriLabel addLinkToURL:[NSURL URLWithString:@"http://github.com/mattt/"] withRange:range];
    [self.view addSubview:attriLabel];
    attriLabel.frame = CGRectMake(20.0, 100.0, 300.0, 200.0);
}

实现TTTAttributedLabelDelegate 代理方法

在如下代理方法中查看当前点击的链接。

//! 实现代理方法
- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithURL:(NSURL *)url {
    
    NSLog(@"url信息:%@", url);
}

TTTAttributedLabel 部分实现

设置 attriLabel.text = @"Fork me on GitHub! (https://github.com/mattt/TTTAttributedLabel/)"; 查看了 TTTAttributedLabel 的大概实现流程。

设置 TTTAttributedLabel 用户交互可用。

self.userInteractionEnabled = YES;

TTTAttributedLabel 链接指定了默认链接样式

TTTAttributedLabel 为链接指定了默认链接样式为蓝色和带下划线。

相关代码为:

NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary];
[mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName];
if ([NSMutableParagraphStyle class]) {
    [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName];
} else {
    [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
}
self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes];

使用 NSDataDetector 检测 label.text 中的链接

NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string] options:0 range:NSMakeRange(0, [(NSAttributedString *)text length])];

NSArray<NSTextCheckingResult *> * 类型的 results 数组中会有 label 中文本的链接信息。

<__NSArrayM 0x2830a6310>(
<NSLinkCheckingResult: 0x283ed27c0>{20, 44}{https://github.com/mattt/TTTAttributedLabel/}
)
// label.text中的URL
(lldb) po ((NSLinkCheckingResult *)[results lastObject]).URL
https://github.com/mattt/TTTAttributedLabel/
// label.text中的URL 的range
(lldb) po ((NSLinkCheckingResult *)[results lastObject]).range
location=20, length=44

"自动检测"链接

当我们设置了 attriLabel.text = @"Fork me on GitHub! (https://github.com/mattt/TTTAttributedLabel/)"; 时,会发现https://github.com/mattt/TTTAttributedLabel/ 自动变为链接形式。自动检测出 label.text 中的文本中有 url 信息是依次在 - (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes- (void)addLinks:(NSArray *)links 的实现中做的处理。

  • 首先通过在- (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes 方法中封装链接文本属性信息媒介 TTTAttributedLabelLink

  • 再通过在- (void)addLinks:(NSArray *)links 方法中根据 TTTAttributedLabelLink 传递过来的文本属性及url 位置信息,设置 label.attributedText 以达到能够自动检测出 label.text 中的 url 的目的。

另外我们自行添加addLinkToURl

- (void)addLinks:(NSArray *)links {
    ...
    self.attributedText = mutableAttributedString;
    ...
}

添加链接到指定 range

[attriLabel addLinkToURL:[NSURL URLWithString:@"http://github.com/mattt/"] withRange:range]; 为例。可以发现 TTTAttributedLabel 内部实现是

[self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]]; 直到

[self addLinkWithTextCheckingResult:result attributes:self.linkAttributes]; 就和上述的“自动检测”链接的内容是一样的。

点击Label 的链接文字后,查找到对应 url

这部分内容主要分为 touchesBegan 方法中查找点击的位置的链接, touchesMoved 方法中比对触摸到Label 上的位置变动后,当前位置的链接和 touchesBegan 方法中找到的链接是否还一样,最后在 touchesEnded 中把点击的链接以为block 和 代理的方式传递出去。

下边简单说明下笔者查看touchesBegan方法中查找点击链接的内容。

  • 获取到当前点击的点
[touch locationInView:self]
  • - (TTTAttributedLabelLink *)linkAtPoint:(CGPoint)point 方法中获取到当前点链接
    • 首先在 - (CFIndex)characterIndexAtPoint:(CGPoint)p 方法中找到点击的位置的字符的索引
    • 然后通过 - (TTTAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx 方法中找到对应字符的索引的TTTAttributedLabelLink实例(其中包含当前点击点的链接信息)

相应代码,详情见 TTTAttributedLabel

[self linkAtPoint:[touch locationInView:self]];
TTTAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]];

获取到当前点击位置的字符的索引

笔者感觉其中难理解的地方为获取到当前点击位置的字符的索引。相关内容如下:

把当前点的坐标转换为对应 UILabel 中的文字坐标转换为针对于UILabel 自身坐标系的点坐标;

p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);

另一个是把iOS 做左上角为原点坐标转换为CT 坐标系的左下角为原点坐标的调整。

p = CGPointMake(p.x, textRect.size.height - p.y);
  • 根据当前label相对自身坐标系 frame 及 属性字符串 创建CT坐标系所需的frame
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, textRect);
    CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), path, NULL);
  • 确定UILabel 当前在CT 坐标系显示占用的行数 CFArrayGetCount(lines)
NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  • 遍历CT 坐标中文字的每一行,找到当前点击点所在的行,计算出点击点相对于当前行的坐标,并计算出当前索引。
CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
idx = CTLineGetStringIndexForPosition(line, relativePoint);

其中还有ascent(字形最高点到baseline的推荐距离) 和descent(字形最低点到baseline的推荐距离) 相关的内容等。笔者不了解,有兴趣的话,可以查看CoreText相关内容,如深入理解Core Text排版引擎

    CFIndex idx = NSNotFound;   

    CGPoint lineOrigins[numberOfLines];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);

    for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
        CGPoint lineOrigin = lineOrigins[lineIndex];
        CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);

        // Get bounding information of line
        CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
        CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);

        // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
        CGFloat flushFactor = TTTFlushFactorForTextAlignment(self.textAlignment);
        CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width);
        lineOrigin.x = penOffset;

        // Check if we've already passed the line
        if (p.y > yMax) {
            break;
        }
        // Check if the point is within this line vertically
        if (p.y >= yMin) {
            // Check if the point is within this line horizontally
            if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
                // Convert CT coordinates to line-relative coordinates
                CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
                idx = CTLineGetStringIndexForPosition(line, relativePoint);
                break;
            }
        }
    }

本文说明了TTTAttributedLabel 的基本使用及部分实现。有兴趣的读者请下载TTTAttributedLabel 查看详情。

参考学习网址


推荐文章:
用SwiftUI给视图添加动画
用SwiftUI写一个简单页面
iOS 控制日志的开关
iOS App中可拆卸一个framework的两种方式
自定义WKWebView显示内容(一)
Swift 5.1 (7) - 闭包
Swift 5.1 (6) - 函数
Swift 5.1 (5) - 控制流
Xcode11 新建工程中的SceneDelegate
iOS App启动优化(二)—— 使用“Time Profiler”工具监控App的启动耗时
iOS App启动优化(一)—— 了解App的启动流程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • iOS开发系列--网络开发 概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可...
    lichengjin阅读 3,637评论 2 7
  • Scrum Scrum is a framework for developing and sustaining ...
    寒十四阅读 254评论 0 0
  • 有一位老妇,她特别喜爱吃洋葱头。她长着一只像洋葱头的鼻子,当她嘴馋的时候,又恰好家里的洋葱头断顿,她会摸着鼻子说:...
    楚莲若阅读 435评论 2 6
  • 本文参考:https://morvanzhou.github.io/tutorials/machine-learn...
    小董不太懂阅读 939评论 0 2
  • 王青在回家的路上捡了一只猫,脏兮兮的,浑身都是泥。对于他为什么要捡它回家,他解释为爱心泛滥,不忍心看着它淋着雨。 ...
    我是小阿旭吖阅读 381评论 0 0