iOS7で新しいAttributedStringの属性NSLinkAttributeNameは追加され、これで簡単にリンクテキスト付けれるのかなと思ってUITableViewCellのtextLabelに貼ってみたところ、UILabelはそもそもユーザーのインタラクションを受け付けてないので押せませんでした。
仕方ないのでCellにUITextViewを配置すると、今度はUITextViewがタッチイベントを奪ってしまいCellが押せませんでした。
この手の実装はOSSであるのでそれを利用するのがいいと思いますがあえて標準のUITextViewで何とかする方法を考えてみました。
実装
方針としては要するにリンクのところ以外のタッチはスルーすればいいので、
タッチ領域から押している文字を探し出し、その文字にNSLinkAttributeNameが指定されているかを確かめます。
iOS7ですのでせっかくですからTextKitのメソッドを幾つか用いて探すことにします。
UITextViewを継承する
まずUITextViewのサブクラスを作成します。
次にTextViewのプロパティを以下のようにします
- editable = NO
- selectable = YES
また、Cell上に置くので下記の設定もします。
- scrollEnabled = NO
リンク領域のみタッチを許可する
次にタッチした文字にリンクあるかどうかを確認します。
ここでタッチしたViewを返す-hitTest:withEventメソッドをオーバーライドします。
// リンクのところ以外のタッチを透過する
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint p = point;
p.y -= self.textContainerInset.top;
p.x -= self.textContainerInset.left;
NSInteger i = [self.layoutManager characterIndexForPoint:p inTextContainer:self.textContainer fractionOfDistanceBetweenInsertionPoints:NULL];
NSRange effectiveRange;
NSDictionary *attr = [self.textStorage attributesAtIndex:i effectiveRange:&effectiveRange];
if (attr[NSLinkAttributeName]) {
__block BOOL touchingLink = NO;
NSInteger glyphIndex = [self.layoutManager glyphIndexForCharacterAtIndex:i];
[self.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(glyphIndex, 1) usingBlock: ^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
if (CGRectContainsPoint(usedRect, p)) {
touchingLink = YES;
*stop = YES;
}
}];
return (touchingLink) ? self : nil;
}
return nil;
}
タップした文字属性の取得方法は以下のとおりです。
- textContainerInsetの左上分原点を移動させる
- NSLayoutManager#characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints で触った文字が何番目の文字なのかを取得
- textStrorageから触った文字の属性を取得
- 触った文字にリンク属性があればタッチした場所がリンクのエリアに含まれているかを確認する。文字列の最後がリンクで終わっている時、テキストの範囲外の下の方を触ると2番目の返り値が必ず最後の文字を指してしまうため
- タッチしているのがリンクの箇所だった場合のみselfを、それ以外はnilを返す
選択領域を出ないようにする
これだけでだいたいそれっぽい挙動になるのですが、リンクのところを二本指で長押しすると選択キャレットが表示されます。なのでselectableプロパティをNOにしたいところですが、これをするとリンクも押せなくなってしまいます。
仕方ないのでセレクト領域を表示しないように変更します。
これはUITextInputプロトコルの実装をオーバーライドします。
// 選択領域を出さないようにする
- (NSArray *)selectionRectsForRange:(UITextRange *)range
{
return nil;
}
こうすることでだいたいそれっぽくなるかなという印象です。
とりあえず下記にgistを作りました
https://gist.github.com/fmtonakai/11144531
次はリンクが押せるLabel見たいのを作ってみようかなと思います。