Help us understand the problem. What is going on with this article?

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

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

peromasamune
iOSがメインです(swift,Objective-C)。たまにAndroid、サーバーサイドなど(PHP, Ruby)
http://peromasamune.hateblo.jp/
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中https://twitter.com/yumemiinc
http://www.yumemi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away