2021年9月26日日曜日

リンクタップ可能でかつ選択不可なUITextViewを作る

次なような、UITextViewを作成することにしました。

1. リンクタップは可能である、その他のテキストは操作無反応とする
2. ダブルタップによるテキスト選択動作は不可である
3. ロングタップは不可である


UITextViewのカスタムViewを作成する

条件1から3まで満たすようなカスタムViewを作ります。
この方法は、Stackoverflowに書かれているものを参考にしました。
UITextView: Disable selection, allow links


class UnselectableTextView: UITextView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard let position = closestPosition(to: point) else { return false }
        guard let range = tokenizer.rangeEnclosingPosition(
                position,
                with: .character,
                inDirection: UITextDirection(rawValue: UITextLayoutDirection.left.rawValue)
              ) else { return false }
        let startIndex = offset(from: beginningOfDocument, to: range.start)
        return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
    }

    override func becomeFirstResponder() -> Bool {
        return false
    }
}


override func point() にて、タップした位置のテキストがリンクであるかどうか判定し、override func becomeFirstResponder() にてダブルタップとロングタップを抑制しています。
ViewControllerに実装するときは、次のようになります。


class ViewController: UIViewController {

    @IBOutlet weak var textView: UnselectableTextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }

    private func setupViews() {
        textView.delegate = self
        textView.isEditable = false
        textView.isSelectable = true
        textView.backgroundColor = .clear

        let attributedString = NSMutableAttributedString(string: "this textview has a link test.")
        attributedString.addAttribute(.link, value: "https://www.google.com", range: NSRange(location: 20, length: 4))
        textView.attributedText = attributedString
    }
}

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


ロングタップをハンドリングする

上記で作成したUnselectableTextViewに、独自の UILongPressGestureRecognizer を組み込もうとしたが断念。
UITextViewの GestureRecognizer とConflictしてしまい、良い感じで実装できずでした。
代替案として、UnselectableTextViewの後ろにUIViewを配置して、このUIViewにてロングタップをハンドリングさせます。


class ViewController: UIViewController {
    private func setupGesture() {
        let longPressGesture = UILongPressGestureRecognizer(
            target: self,
            action: #selector(ViewController.longPress(_:))
        )
        longPressGesture.delegate = self
        backgroundView.addGestureRecognizer(longPressGesture)
    }
}

extension UIViewController: UIGestureRecognizerDelegate {
    @objc func longPress(_ sender: UILongPressGestureRecognizer) {
        print(sender.state.rawValue.description)
    }
}