Twitter iOSアプリのテキスト入力を支える TwitterTextEditor を試してみた

f:id:temoki-ht:20210201180005p:plain

こんにちは。スタメンでTUNAGやFANTSのモバイルアプリ開発を担当している @temoki です。

先週、Twitter iOSアプリで使用されているテキストエディタが TwitterTextEditor というOSSとして公開され、iOSアプリエンジニアの間で話題になりましたね。以下がTwitter公式のエンジニアリングブログによる紹介記事です。

blog.twitter.com

私はTUNAG iOS/Androidアプリの開発の中で、メンションや絵文字のショートコード入力を備えたテキスト入力機能の実装に苦労してきていることもあり、このOSSの公開はとても興味をそそられるものでしたので、早速試してみることにしました。今回のブログでは、実際にこの TwitterTextEditor を利用するという観点で書きたいと思います。

以降の内容は TwitterTextEditor v1.0.0 時点での内容となります。TwitterTextEditor の概要につきましては、上記の公式ブログやそれを日本語で紹介されている以下の記事をご覧ください。

TwitterがiOSアプリ向けに新しいオープンソースのテキストエディタAPI「Twitter Text Editor」を発表 - GIGAZINE

TwitterTextEditorの機能

公式ブログでは TwitterTextEditor の機能として以下の5つについて挙げられていますので、1つ1つ実際のコードなども交えながら紹介していきます。

  • Easy delegate-based APIs
  • Robust text-attribute update logic
  • Additional text editing events
  • Safe event handling for text input
  • Support for recent versions of iOS

Easy delegate-based APIs

TwitterTextEditor は UIKit と同じようなデリゲートベースのAPIが提供されています。例えばテキスト入力の開始・終了のイベントは TextEditorViewEditingDelegate というプロトコルを実装することでハンドルすることができますが、これは UITextViewDelegate のそれとほぼ同じになっていることがわかります。

public protocol TextEditorViewEditingDelegate: AnyObject {

    func textEditorViewShouldBeginEditing(_ textEditorView: TextEditorView) -> Bool

    func textEditorViewDidBeginEditing(_ textEditorView: TextEditorView)

    func textEditorViewDidEndEditing(_ textEditorView: TextEditorView)

}

というのも TwitterTextEditor は UITextView を内包しているため UITextView で提供されている機能はほぼ網羅されているのです。そのため、既存のプロジェクトですでに UITextView を組み込んでテキスト入力を実装している箇所も容易に置き換えることができそうです。

TwitterTextEditor はイベントの種類に応じて複数のプロトコルに分割されています。

textEditorView.font = UIFont.systemFont(ofSize: 15)
textEditorView.keyboardType = .default
textEditorView.textContentInsets = .init(top: 10, left: 10, bottom: 10, right: 10)
textEditorView.placeholderText = "メッセージを入力" // プレースホルダー対応😂
textEditorView.editingDelegate = self

そして個人的に嬉しいのは、プレースホルダーの表示に対応している点です。同じ UIKit の UITextField はプレースホルダーの表示に対応しているのに、なぜか UITextView は対応していなくて、泣く泣く実装する...ということもなくなりますね。

Robust text-attribute update logic

ここからが TwitterTextEditor が本領発揮する部分となります。TwitterTextEditor はテキスト属性を更新するためのAPIを提供しており、例えばシンタックスハイライトのような機能を実装しやすくなっています。

Twitterアプリでいうとメンションやハッシュタグのハイライト表示ですね。例えば Markdown テキストの入力に対する簡単なプレビュー機能なんかにも応用できそうです。試しに二つのアスタリスクで囲んで強調表示する記法( **text strong emphasis** ) で実装してみます。

テキスト属性を更新できるタイミングで TextEditorViewTextAttributesDelegate プロトコルのメソッドが呼び出されますので、ここで属性を更新して返します。更新結果は completion ハンドラを介して返すことになるので、バックグラウンドでテキストの解析と属性更新を行えるのがポイントです。

extension EditorViewController: TextEditorViewTextAttributesDelegate {
    func textEditorView(_ textEditorView: TextEditorView, updateAttributedString attributedString: NSAttributedString, completion: @escaping (NSAttributedString?) -> Void) {
        // バックグラウンドでテキストを解析して属性を更新する
        DispatchQueue.global().async {
            // テキストの解析
            let regex = try! NSRegularExpression(pattern: "(\\*+)(\\s*\\b)([^\\*]*)(\\b\\s*)(\\*+)", options: [])
            let stringRange = NSRange(location: 0, length: attributedString.length)
            let matches = regex.matches(in: attributedString.string, options: [], range: stringRange)
            
            // テキスト属性の更新
            let newAttributedString = NSMutableAttributedString(attributedString: attributedString)
            newAttributedString.removeAttribute(.font, range: stringRange)
            newAttributedString.addAttribute(.font, value: UIFont.systemFont(ofSize: 15), range: stringRange)
            for match in matches {
                newAttributedString.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 15), range: match.range)
            }

            // メインスレッドで更新結果を返す
            DispatchQueue.main.async {
                completion(newAttributedString)
            }
        }
    }
}

実行結果の動画です。

f:id:temoki-ht:20210201162712g:plain

いい感じですね。ただ、Markdown は多くの記法があり、編集するテキストのサイズも大きくなる可能性が高いです。実際に Markdown エディタを実装する場合、このTwitterTextEditorの機能がすべてを解決するものではないことを理解しておく必要があります。開発者の @niw さんもTwitterで次のようなことをおっしゃっています。

Additional text editing events

Twitterは世界中で利用されているアプリですので、あらゆる言語での入力に対応しなければなりません。そのため、TwitterTextEditor には多言語への対応を考慮したテキスト入力イベントが追加されています。以下がそのイベントです。

  • 入力中の言語が切り替わった時のイベント
  • テキスト入力の方向が切り替わった時のイベント(アラビア語などの Right-to-left writing な言語への考慮)

これは TextEditorViewTextInputObserver プロトコルとして提供されています。このイベントをトリガーに、入力のためのUIの切り替えなどに使われることを想定されているようです。

public protocol TextEditorViewTextInputObserver: AnyObject {

    func textEditorView(_ textEditorView: TextEditorView,
                        didChangeInputPrimaryLanguage inputPrimaryLanguage: String?)

    func textEditorView(_ textEditorView: TextEditorView,
                        didChangeBaseWritingDirection writingDirection: NSWritingDirection)

}

Safe event handling for text input

例えば、入力済の文字数を表示したり、入力中のテキスト内容に応じた入力サジェストなどを行うためには、ユーザーのテキスト入力イベントを使用します。 UITextView でこのイベントをハンドリングするには、UITextViewDelegate の以下のデリゲートメソッドを使うことになります。

func textView(UITextView, shouldChangeTextIn: NSRange, replacementText: String) -> Bool
func textViewDidChange(UITextView)

TwitterTextEditor ではこれらよりも安全にテキスト入力イベントをハンドリングするための TextEditorViewChangeObserver プロトコルが提供されています。 TwitterTextEditor のソースコードの中には // UIKit behavior ~ というコメントで、UIKit の動作の問題点とそのワークアラウンドといった情報がたくさん記述されており、これらをふまえた上で安全なイベントを提供してくれるというわけですね。

public protocol TextEditorViewChangeObserver: AnyObject {
    func textEditorView(_ textEditorView: TextEditorView,
                        didChangeWithChangeResult changeResult: TextEditorViewChangeResult)
}

iOSアプリでのテキスト入力の難しさやその解決方法については、作者の @niw さんが昨年の iOSDC Japan 2020 でも発表されていますので、ぜひご覧いただきたいです。

www.youtube.com

このプレゼンテーションの中では、テキスト入力を伴うアプリを開発する場合、ソフトウェアキーボードの表示状態の変更についても気にしながら実装する必要があるという点についても言及されており、これについては同じく @niw さんが公開されている KeyboardGuide というライブラリでスマートに解決することができるので、こちらもオススメです。

GitHub - niw/KeyboardGuide: A modern, real iOS keyboard system notifications handler framework that Just Works.

Support for recent versions of iOS

TwitterTextEditor はサポートバージョンとして iOS 11 以降という良心的な設定になっています(Twitter iOS アプリの最小バージョンが iOS 12 なのに!)。

そして、サポートされているパッケージマネージャーも CocoaPods、Carthage、Swift Package Manager と一般的なものは全て揃っているので導入で困るということはなさそうですね。

おわりに

世界中で利用される Twitter iOS アプリ。そのテキスト入力を支えるOSS、TwitterTextEditor についてご紹介いたしましたが、いかがでしたでしょうか。

このブログを書くにあたって TwitterTextEditor のソースコードを眺めてみましたが、とても勉強になる部分が多かったり、作者の実装の苦労が垣間見えたりしました。また、自分が TUNAG アプリの開発で実装したコードに類似しているところをいくつか見つけて親近感が沸いたりもしたので、改めて人のソースコードを読むのは重要なことだなと感じました。


最後になりましたがスタメンでは TUNAG や FANTS そして新しい事業におけるプロダクト開発を一緒に牽引してくれる仲間を募集しています。エンジニアに限らず、デザイナーやプロダクトマネージャー職も募集中ですので、興味のある方は下記の応募からご連絡ください!