UITextView を使ってテキストの一部をハイパーリンク化する

環境: Swift3
iOS10

f:id:xyk:20170518200531g:plain

UILabel ではなく UITextView の attributedText に NSLinkAttributeName をセットすることで簡単にできた。
クリック時にデフォルトでは Safari が起動して設定したURLのページが表示された。
クリック時の挙動を変更したい場合は UITextViewDelegate の
func textView(UITextView, shouldInteractWith: URL, in: NSRange, interaction: UITextItemInteraction)(iOS 10から)
または
func textView(UITextView, shouldInteractWith: URL, in: NSRange) (iOS7,8,9はこちら。iOS 10でDeprecated)
を実装して制御する。
今回は SFSafariViewController で開いてみた。
以下が実装例。

import UIKit
import SafariServices

class ViewController: UIViewController, UITextViewDelegate {

    @IBOutlet weak var textView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTextView()
    }
    
    func setupTextView() {
        
        let text = "詳細はこちらをご覧ください"
        
        textView.delegate = self
        textView.isSelectable = true
        textView.isEditable = false
        textView.textContainer.lineFragmentPadding = 0
        textView.textContainerInset = .zero
        textView.isScrollEnabled = false // これが必要な理由は後述
        
        let attributedString = NSMutableAttributedString(string: text)
        let range = NSString(string: text).range(of: "こちら")
        
        attributedString.addAttribute(
            NSLinkAttributeName,
            value: "https://www.google.co.jp/",
            range: range)
        
        textView.attributedText = attributedString
        textView.linkTextAttributes = [NSForegroundColorAttributeName: UIColor.red]
    }

    // MARK: - UITextViewDelegate
    
    // この Delegate の実装しない場合はデフォルトで URL を Safari で開く。
    func textView(_ textView: UITextView,
                  shouldInteractWith URL: URL,
                  in characterRange: NSRange,
                  interaction: UITextItemInteraction) -> Bool {
        
        // UIApplication.shared.open(URL)
        let controller = SFSafariViewController(url: URL)
        self.present(controller, animated: true)
        
        return false
    }

}

これでテキストの一部をハイパーリンク化できる。

UITextView のレイアウト制約について

ここからはハイパーリンク化とは関係のない UITextView の挙動でちょっとハマった話。
上記例での UITextView のレイアウトは Storyboard の AutoLayout を使って行っていたのだが、UITextView の高さの制約を指定しないと制約エラーになった。
バイスによって横幅が変わるので、高さの制約はテキストの長さに応じて自動的に設定して欲しいところ。
UILabel であればテキストが長くて複数行に渡る場合でも intrinsicContentSize が自動的に設定されるので AutoLayout で高さの制約は指定しなくてもよい。
UITextView も同じような挙動を期待していたが、 intrinsicContentSize を見てみたところ、-1(UIViewNoIntrinsicMetric)でサイズ不定となっていたため、高さの制約指定が必要であった。
ただし、これは UITextView の isScrollEnabled プロパティを false とすること(デフォルトはtrue)で intrinsicContentSize が自動設定され、高さの制約指定は不要になることがわかった。
つまり、UITextView は UIScrollView のサブクラスであるため、isScrollEnabled=trueの場合は、枠となる矩形のサイズと、内部のスクロールさせる矩形のサイズは違うため、枠となる矩形側の高さの制約指定は必要となり、isScrollEnabled=falseにすればスクロール不要となるので UILabel と同様に高さが自動的に設定されるのだと思われる。

UITextView の下側のテキストが切れてしまう件

これも余談だが、UITextView に複数行に渡る長いテキストが設定されている場合に下側のテキストが切れてすべて表示されないことがあった。
これはデフォルトのシステムフォントを使っている場合には起きないのだが、ヒラギノフォントを使った場合に発生した。
解決法としては

textView.isScrollEnabled = false
textView.isScrollEnabled = true

というように一旦、isScrollEnabled = falseすればよいらしい。
たしかにこれで解消された。

https://stackoverflow.com/questions/18696706/large-text-being-cut-off-in-uitextview-that-is-inside-uiscrollview