UITextViewで複雑なHighlightを表現する

  • 1
    いいね
  • 0
    コメント

UITextViewで複雑なハイライトを表現する

最近多くのアプリでハッシュタグやメンション、特定の文字列へのハイライトなどをUITextViewで表現したいときが多々あります。
ここではそんなときに役立つライブラリを共有したいと思います。

UITextViewで文字を装飾する

UITextViewを用いて文字を装飾したい場合にはAttributedStringで属性と値を指定することで実現します。例えば、文字を赤色に装飾したい場合には以下のようになります。

textView.text = "Hello World"
let attributedText =  textView.mutableCopy() as? NSMutableAttributedString
attributedText.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: NSMakeRange(0, 5))
textView.attributedText = attributedText

これで無事Helloの部分を赤く表示することができました。しかし、コードをよくみるといくつか辛い部分があります。

実装における問題点

  • NS***AttributeNameというグローバルな値が属性のキーになっているため一覧性がない
  • addAttributeのvalueはAny型となっているためUIColorが設定されることが自明でありながら.redなどの省略が行えない。また、どんな値でも引数に渡せてしまい安全でない。
  • NSMutable~, NSRange, NS***AttributeNameなどObjective-Cの遺産を多く使用しており今後リプレースされるとしても既存実装から新たに実装が必要となる。

こういった観点からなかなか実装しにくいと言う方も、もしかしたらいるかもしれません。そういった方はHTMLで表現したりライブラリの導入検討を行なったかと思います。

また、上記の例ではシンプルに色を装飾するだけでしたが実際にはテキストの中からある特定のキーワードを個別に全てハイライトしたい場合や、キーワードごとにリンクしタップ時の挙動を柔軟に変更したいケースがほとんどです。※調べるといくつかでてきますがリンクにURL形式の文字列を埋め込み、DelegateでSchemeを検出して取得する方法もありますが文字列化されたリンクの正しい使い方と呼ぶには難しそうです。

Swift3ではUITextViewのプロパティとしてlinkTextAttributesというものがありますが、これはリンク属性の付いたテキストの見た目を一括で装飾の指定を行うことができますが、例えば、ハッシュタグは青色、メンションは赤色など個別に対応した属性の指定を行うことはできません。

// 単語ごとに個別の指定は行えない
textView.linkTextAttributes = [NSForegroundColorAttributeName: UIColor.red]

そこで今回上記の問題点を解決したく作成したライブラリがこちらです。

RegeributedTextView

RegeributedTextViewは、装飾の柔軟性と扱いやすさを意識し正規表現ベースで型安全なインターフェースとなるように作られています。先ほどのテキストを赤くする場合の例をRegeributedTextViewを用いて行うと次のようになります。

textView.addAttribute("Hello", attribute: .textColor(.red))

RegeributedTextViewで属性を付与する関数であるaddAttributeは下記のように宣言されています。

func addAttribute(_ regexString: String, 
                  attribute: TextAttribute,
                  values: [String: Any] = [:], 
                  priority: Priority = .medium, 
                  applyingIndex: ApplyingIndex = .all)

ここではより複雑な表現を指定するためのpriorityapplyingIndexについては割愛させていただき、regexStringattributevaluesについて説明したいと思います。

まずregexStringについてですが、これは属性を付与するテキストを指定する正規表現やテキスト、テンプレートをしていできます。RegeributedTextViewでは主に3つのパターンで使用することができます。上から順に柔軟性が高い指定方法となっています。

パターン1

// 対象の文字列を指定する
textView.addAttribute("#general", attribute: .textColor(.red))

パターン2

// RegexStringTypeから選択する
textView.addAttribute(.hashTag, attribute: .textColor(.red))

パターン3

// 正規表現で指定する
textView.addAttribute("#[a-zA-Z0-9]", attribute: .textColor(.red))

次に引数で指定できるvaluesについてですが、これは属性を付与したテキストに持たせておきたい情報を任意で指定することが可能となっています。また、delegateを実装することで属性の付いたテキスト(各文字・単語レベルで検出)をタップした際にvaluesで指定した値を取得することができます。

//Delegateを指定:(例)class HogeViewController: RegeributedTextViewDelegate
textView.delegate = self

// Value付きで装飾を行う
textView.addAttribute("#general", attribute: .textColor(.red), value: ["URL": "https://hoge.com"])

// 属性テキストがタップされた時に呼ばれるDelegate
func regeributedTextView(_ textView: RegeributedTextView, didSelect text: String, values: [String: Any]) {
        guard let url = values["URL"] as? String else { return }
        print(url)
        // Do something
}

あるテキストに複数の装飾を追加したい場合には下記のようにも記述することが可能です。

textView.addAttributes("#general", attributes: [.bold, .fontSize(16), .textColor(.black)])

まとめ

  • UITextViewで複雑な装飾をしたい場合には実装が大変。
  • RegeributedTextViewでは正規表現による柔軟に装飾が可能(今後Appleの仕様変更による実装のやり直しから解放される)

ライブラリについて新たに追加したい部分や提案などありましたらIssue,PRをいただけると嬉しいです。ハイライトのやり方で迷った方は一度試していただけると幸いです。
最後まで読んでいただきありがとうございました。

参考