Edited at

UITextViewのLink選択以外のイベントを制御する

More than 1 year has passed since last update.


はじめに

UILabelでURLなどのリンクを装飾して、WebViewやSafariに遷移させたい場合、昔はOHAttributedLabelTTTAttributedLabelによくお世話になっていました。

UITextViewのdataDetectorTypesで自動でリンクや電話番号などを選択できるような機能がありますが、UITextViewの性質上コピーや範囲選択などができてしまうので、それらを除外したものを作れば色々と使える場面が出てくるのではないかと思い、作ってみました。


実装

public class LinkTextView : UITextView, UITextViewDelegate {

//MARK: - Properties
//MARK: Public
public var linkEnabled : Bool = true {
didSet{
self.dataDetectorTypes = (linkEnabled) ? .Link : .None
}
}

public var clickHandler : ((url : NSURL) -> Void)?

//MARK: Private

//MARK: - Initializer
public init(frame : CGRect) {
super.init(frame: frame, textContainer: nil)

self.backgroundColor = UIColor.clearColor()
self.delegate = self
self.scrollEnabled = false
self.editable = false
self.selectable = true
self.textContainer.lineFragmentPadding = 0
self.textContainerInset = UIEdgeInsetsZero
self.dataDetectorTypes = .Link
}

required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

//MARK: - Public method
public func setLinkClickHandler(handler : ((NSURL) -> Void)?) {
self.clickHandler = handler
}

//MARK: - Touch event
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
var p = point
p.y = p.y - self.textContainerInset.top
p.x = p.x - self.textContainerInset.left

let i = self.layoutManager.characterIndexForPoint(p, inTextContainer: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let attr = self.textStorage.attributesAtIndex(i, effectiveRange: nil)

var touchingLink = false
if attr[NSLinkAttributeName] != nil {
let glyphIndex = self.layoutManager.glyphIndexForCharacterAtIndex(i)
self.layoutManager.enumerateLineFragmentsForGlyphRange(NSMakeRange(glyphIndex, 1), usingBlock: { (recr, usedRect, container, glyphRange, stop) -> Void in
if CGRectContainsPoint(usedRect, p) {
touchingLink = true
stop.initialize(true)
}
})
return (touchingLink) ? self : nil
}
return nil
}

//MARK: - UITextViewDelegate
public func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
self.clickHandler?(url : URL)
return false
}
}

まず、initializerでUITextViewのプロパティの設定をします。

ここで注意するのが、selectableをfalseにしてしまうとリンクを選択した時のイベントも無効になってしまいます。

hitTestメソッドをオーバーライドしてリンクを選択した時以外のイベントをスルーするようにします。

UITextViewDelegateshouldInteractWithURLでリンクを選択した時のイベントが取得できますが、デフォルトではSafariに遷移するようになっているので、そのままでよければここの実装は不要です。

今回はhandlerで選択したリンクを受け取りたかったので、handlerで値を返して、delegateにはfalseを返しています。


使い方

UITextViewのデフォルトで付いてるpaddingも0にしているので、UILabelのように使えます。

sizeToFitboundingRectWithSizeでラベルの高さをフィットさせることもできます。

let textView = LinkTextView(frame: CGRectMake(0, 0, frame.width, 100))

textView.text = "https://google.com"
textView.setLinkClickHandler({ (url) in
print("url : \(url.absoluteString)")
})
self.addSubview(textView)


終わりに

UILabelでdataDetectorTypesを指定できるようになればいいなぁと作りながら思っていました。

こんなことしなくても便利ライブラリを使ってしまえばいいのですが、Objective-Cの頃に比べてSwiftはバージョン依存なども多く、バージョンが上がると使えなくなったりアップデートされていないと手詰まりになったりするので、Swiftで外部ライブラリを使うのは毎回気を使うので、自作できるものは極力自作するようにしています。