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


# はじめに

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

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

# 実装

```swift:
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メソッドをオーバーライドしてリンクを選択した時以外のイベントをスルーするようにします。

`UITextViewDelegate`の`shouldInteractWithURL`でリンクを選択した時のイベントが取得できますが、デフォルトではSafariに遷移するようになっているので、そのままでよければここの実装は不要です。
今回はhandlerで選択したリンクを受け取りたかったので、handlerで値を返して、delegateにはfalseを返しています。

# 使い方

UITextViewのデフォルトで付いてるpaddingも0にしているので、UILabelのように使えます。
`sizeToFit`や`boundingRectWithSize`でラベルの高さをフィットさせることもできます。

```swift:
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で外部ライブラリを使うのは毎回気を使うので、自作できるものは極力自作するようにしています。