iOS 小坑记录:Undeclared type 与 NSLayoutManager

真没想到这个系列能这么快更新第二篇,果然我的基础不灵。另外这两天踩的小坑真是多啊…

今天接了一个小任务:扩展 UILabel ,支持链接和 @username 的识别,以及点击处理。

由于之前 TTTAttributedLabel 出现了解决不了(懒得解决)的 bug ,所以我打算自己写一个。

链接识别用 NSDataDetector ,@username 识别用正则表达式,点击用 UITapGestureRecognizer ,估计一百多行的规模,似乎没什么难度,结果竟然连踩两坑。

首先来看一个 Swift 的 undeclared type 的问题。

先写 Protocol ,再写类,结果就提示 undeclared type。

我被困扰了一个多小时,甚至一度打算用 ObjC 写了。直到我看函数命名有点刺眼,就顺手把 Protocol 里面的函数改了个命名,把 func STLabelWithLink(... 改成了 func stLabelWithLink(... ,结果它就好了!

然后下一个问题是,UILabel 里面点击的识别。思路这样:先扫描这段文字里面的链接,把它们的 Range 保存在一个数组里。然后给 Label 加一个 UITapGestureRecognizer ,在回调里面把点击的 location 翻译成文字的 index ,判断这个 index 在不在那些 Range 的里面,在的话就调用对应的 handler 。

问题出在把 location 转成 index 的部分。

首先从网上抄一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let location = tapGesture.locationInView(tapGesture.view)

let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines

let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)

let textStorage = NSTextStorage(attributedString: attributedText)
textStorage.addLayoutManager(layoutManager)

let index = layoutManager.characterIndexForPoint(location, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil);

然后爽快地失败了。给 index 打上 Log ,结果除了第一行结果正确,第二行往下点哪里都是 468 。敏锐的我发现文字就是 468 个,所以 layoutManager 把所有字都放在第一行了?验证一下。在绘图的时候,让 layoutManager 也绘一份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override func drawTextInRect(rect: CGRect) {
super.drawTextInRect(rect)

guard let attributedText = attributedText else { return }

let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines

let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)

let textStorage = NSTextStorage(attributedString: attributedText)
textStorage.addLayoutManager(layoutManager)

layoutManager.drawGlyphsForGlyphRange(NSRange(location: 0, length:textStorage.length), atPoint: CGPoint(x: 0,y: 0))
}

结果是这样的:

我再次敏锐的地现,第一行字体粗得不太自然,很明显 layoutManager 画出来的东西和 UILabel 画出来的的东西重了。对比 UILabel 画出的字,可以发现他俩断句是不同的。

layoutManager 的换行是在这里设置的:

1
textContainer.lineBreakMode = lineBreakMode

对于相同的 NSLineBreakMode.ByTruncatingTail ,UILabel 和 NSLayoutManager 的换行却是不同的,UILabel 换行的形式更接近 NSLineBreakMode.ByWordWrapping 。于是把 UILabel.lineBreakMode 设成 NSLineBreakMode.ByWordWrappinglayoutManager 就会按词换行,点击的位置也就能正确翻译成索引了。