リンクのみタップ＆選択可能な UITextView


`UITextView` 中の、リンクのタップ可と、テキストの選択不可を両立させる方法です！

# `UITextView` にタップ可能なリンクを挿入すると…

<details><summary>リンクをタップできる`UITextView`のサンプルコード (タップで開く)</summary><div>

```swift:リンクをタップできるUITextView
import UIKit

class LinkTextViewController: UIViewController {
    let textView: UITextView = {
        let textView = UITextView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        textView.isSelectable = true
        textView.isEditable = false
        let text = "リンク集\nYahoo!\nGoogle"
        let attributedString = NSMutableAttributedString(string: text)
        attributedString.addAttribute(.font,
                                      value: UIFont.systemFont(ofSize: 32),
                                      range: NSRange.init(location: 0, length: attributedString.length))
        attributedString.addAttribute(.link,
                                      value: "https://www.yahoo.co.jp/",
                                      range: NSString(string: text).range(of: "Yahoo!"))
        attributedString.addAttribute(.link,
                                      value: "https://www.google.com/",
                                      range: NSString(string: text).range(of: "Google"))
        textView.attributedText = attributedString
        return textView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(textView)
        textView.center = view.center
    }
}

extension UIViewController: UITextViewDelegate {
    public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        UIApplication.shared.open(URL)
        return false
    }
}
```

</div></details>


リンクをタップ可能にしたい場合には、`isSelectable = true` とする必要があるのですが、
そうするとテキストが選択可能になってしまう :scream: 

<img width="394" alt="Screen Shot 2019-03-10 at 21.08.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/194192/747c9a26-b29b-372e-b587-f85bfb082d9b.png">


# リンクのみタップ＆選択可能にする

下記のように、`.link` 属性しかタップおよび選択できないようにし、
さらに、`.link` 属性が選択されたとしても、メニューを出さないようにすることで、
リンクだけがタップ可能な (他のテキストは選択できない) `UITextView` とすることができます :smile: 

```swift:リンクがタップでき、テキストは選択できないUITextView
    class MyTextView: UITextView {
        // NOTE: リンクテキストしかタップおよび選択できないようにする
        override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
            guard let position = closestPosition(to: point),
                let range = tokenizer.rangeEnclosingPosition(position, with: .character, inDirection: UITextLayoutDirection.left.rawValue) else {
                    return false
            }
            let startIndex = offset(from: beginningOfDocument, to: range.start)
            return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
        }

        // NOTE: テキストが選択された場合に、コピーなどのメニューを出さないようにする
        override func becomeFirstResponder() -> Bool {
            return false
        }
    }
```


<details><summary>リンクのみタップ＆選択できる`UITextView`のサンプルコード (タップで開く)</summary><div>

```swift
import UIKit

class LinkTextViewController: UIViewController {
    class MyTextView: UITextView {
        // NOTE: リンクテキストしかタップおよび選択できないようにする
        override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
            guard let position = closestPosition(to: point),
                let range = tokenizer.rangeEnclosingPosition(position, with: .character, inDirection: UITextLayoutDirection.left.rawValue) else {
                    return false
            }
            let startIndex = offset(from: beginningOfDocument, to: range.start)
            return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
        }

        // NOTE: テキストが選択された場合に、コピーなどのメニューを出さないようにする
        override func becomeFirstResponder() -> Bool {
            return false
        }
    }

    let textView: MyTextView = {
        let textView = MyTextView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        textView.isSelectable = true
        textView.isEditable = false
        let text = "リンク集\nYahoo!\nGoogle"
        let attributedString = NSMutableAttributedString(string: text)
        attributedString.addAttribute(.font,
                                      value: UIFont.systemFont(ofSize: 32),
                                      range: NSRange.init(location: 0, length: attributedString.length))
        attributedString.addAttribute(.link,
                                      value: "https://www.yahoo.co.jp/",
                                      range: NSString(string: text).range(of: "Yahoo!"))
        attributedString.addAttribute(.link,
                                      value: "https://www.google.com/",
                                      range: NSString(string: text).range(of: "Google"))
        textView.attributedText = attributedString
        return textView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(textView)
        textView.center = view.center
    }
}

extension UIViewController: UITextViewDelegate {
    public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        UIApplication.shared.open(URL)
        return false
    }
}
```

</div></details>

どや！テキスト選択できまい！

<img width="396" alt="Screen Shot 2019-03-10 at 21.37.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/194192/1ad1bc12-10d6-fea2-e828-112a53f8e248.png">



# 余談

別に選択できても良いとは思うけれども、
リンクがあるところのテキストだけ選択可能になっていると、ちょっとね…。


# 参考

- [SwiftでUITextViewにリンクを挿入する](https://qiita.com/shtnkgm/items/0009ef445a96126d7b16)
- [UITextView: Disable selection, allow links](https://stackoverflow.com/questions/36198299/uitextview-disable-selection-allow-links)
- [UITextView link selectable without rest of text being selectable](https://stackoverflow.com/questions/27988279/uitextview-link-selectable-without-rest-of-text-being-selectable)