LoginSignup
7
5

More than 5 years have passed since last update.

【Swift+Extension】UITextViewで、ハイパーリンクに対応する

Last updated at Posted at 2018-03-27
UITextView+Link.swift
import UIKit

extension UITextView {

    /// 対象の文字列に対して、リンクを付加する
    ///
    /// - Note:
    /// textView(_:shouldInteractWith:in:interaction:) は呼び出し側で実装して下さい
    ///
    /// - Parameters:
    ///   - pattern: 対象の文字列
    ///   - urlString: URL
    ///   - color: リンク色
    func addLink(pattern: String, urlString: String = "", color: UIColor) {
        _ = _addLink(pattern: pattern, urlString: urlString, color: color)
    }

    /// 対象の文字列に対して、リンクを付加する
    ///
    /// - Note:
    /// LinkTextViewDelegateRepresents のインスタンスは呼び出し側で保持して下さい(保持しないと自動解放されるため)
    ///
    /// - Parameters:
    ///   - pattern: 対象の文字列
    ///   - urlString: URL
    ///   - color: リンク色
    ///   - action: アクション
    /// - Returns: Text view delegateを象徴するクラス
    func addLink(pattern: String, urlString: String = "", color: UIColor, action: @escaping LinkTextViewDelegateRepresents.Action) -> LinkTextViewDelegateRepresents? {
        return _addLink(pattern: pattern, urlString: urlString, color: color, action: action)
    }

    private func _addLink(pattern: String, urlString: String = "", color: UIColor, action: LinkTextViewDelegateRepresents.Action? = nil) -> LinkTextViewDelegateRepresents? {

        // Configure
        isEditable = false
        isSelectable = true
        isUserInteractionEnabled = true
        textContainer.lineFragmentPadding = 0
        textContainerInset = .zero

        // Add Color
        linkTextAttributes = [NSAttributedStringKey.foregroundColor.rawValue: color]

        // String
        let strings = [attributedText?.string, text].flatMap { $0 }
        guard let string = strings.first else { return nil }

        // Ranges
        let nsRanges = string.nsRanges(of: pattern, options: [.literal])
        if nsRanges.count == 0 { return nil }

        // Add Link
        let attributedString = attributedText != nil
            ? NSMutableAttributedString(attributedString: attributedText!)
            : NSMutableAttributedString(string: string)

        for nsRange in nsRanges {
            attributedString.addAttributes([.link: urlString], range: nsRange)
        }

        // Set Text
        attributedText = attributedString

        // Return
        if let action = action {
            return LinkTextViewDelegateRepresents(action: action)
        } else {
            return nil
        }
    }

}
LinkTextViewDelegateRepresents.swift
import UIKit

final class LinkTextViewDelegateRepresents: NSObject {

    typealias Action = (_ textView: UITextView, _ url: URL, _ characterRange: NSRange, _ interaction: UITextItemInteraction) -> Bool

    var action: Action

    init(action: @escaping Action) {
        self.action = action
    }
}

extension LinkTextViewDelegateRepresents: UITextViewDelegate {

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        return action(textView, URL, characterRange, interaction)
    }
}
String+Range.swift
import Foundation

extension String {

    func nsRange(from range: Range<String.Index>) -> NSRange {
        return NSRange(range, in: self)
    }

    func ranges(of searchString: String, options mask: NSString.CompareOptions = [], locale: Locale? = nil) -> [Range<String.Index>] {
        var ranges: [Range<String.Index>] = []
        while let range = range(of: searchString, options: mask, range: (ranges.last?.upperBound ?? startIndex)..<endIndex, locale: locale) {
            ranges.append(range)
        }
        return ranges
    }

    func nsRanges(of searchString: String, options mask: NSString.CompareOptions = [], locale: Locale? = nil) -> [NSRange] {
        let ranges = self.ranges(of: searchString, options: mask, locale: locale)
        return ranges.map { nsRange(from: $0) }
    }

}

使い方(1) - Delegateで実装する場合

let textView = UITextView()
textView.text = "Googleのリンク"
textView.addLink(pattern: "Google", urlString: "https://www.google.com", color: .blue)

class LinkSampleViewController: UIViewController, UITextViewDelegate {

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        return true
    }

}

使い方(2) - Closure(Action)で実装する場合

let textView = UITextView()
textView.text = "Googleのリンク"

let linkRepresents = textView.addLink(pattern: "Google", urlString: "https://www.google.com", color: .blue) { (textView, url, characterRange, interaction) -> Bool in
    return true
}

ワンポイント

  • Configureで、UILabelと同等な見た目/UIにしているのがポイント
    • テキスト編集不可にする。タップ時にキーボードが表示されるのを防ぐ
      • isEditable = false
    • リンクタップイベントをハンドリングする
      • isSelectable = true
      • isUserInteractionEnabled = true
    • 余白を0にする
      • textContainer.lineFragmentPadding = 0
      • textContainerInset = .zero
7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5