Shift + EnterでUITextViewを改行


SmartKeyboardなども発売され、iPadにはずっとキーボードを接続して利用している。というユーザーも多いのではないかと思いますが、そのような市場の様子を見て「enterを確定や直下の新規作成操作とし、shift + enterで改行操作」と思った場合、素直に対応したAPIがあれば良いですがUITextViewDelegateには該当するものが存在しないため、少し工夫をする必要があります。

この記事ではその実現方法について解説します。

方針

こちらのStackoverflowの質問が参考にしましたが、

「UIKeyCommandをoverrideしてインターセプトし、shift + enterで独自の改行アクションを実行する」という方針になります。

実装

コードはこちらのgistに投稿しましたので参考にしてください。

今回はUITextViewのサブクラスを利用して実現を図りました。

ポイント1: keyCommandsのoverride

システム定義以外のあるキーが押されたとき、システムはresponderChainを探して該当するキーに対応するUIKeyCommandが実装されているか確認し、存在すればそのアクションを実行します。

以下の実装では、shift + return(enter)を受け取るとnewLine(sender:)というアクションを実行するようなコマンドを返しています。

override var keyCommands: [UIKeyCommand]? {
// キーボードへのinputは、returnなので"\r". うっかり"\n"としてしまう事があるが、それは押下したキーではないため.
        return [UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(newLine(sender:)))]
 }

UITextViewDelegatetextView(_:shouldChangeTextIn:replacementString:)を実装している場合であっても、このkeyCommandsの実装がインターセプトするため、テキスト入力まで辿り着かずデリゲートメソッドは呼ばれません。

なので、任意の文字列を追加した後にデリゲートが動いて欲しいときは、手動で呼んであげるようにすると良いでしょう。

ポイント2: アクションの実装

UITextViewtextプロパティに直接\nを代入したいところですが、firstResponderが外れてしまう上に予期しない挙動を引き起こしてしまいます。

そのため、現在アクティブなUITextView/UITextFieldのテキストに変更を加える場合は、UIKeyInputプロトコルに属しているinsertText(_:)メソッドを利用しましょう。

このメソッドは、テキストの現在カーソルが当たっているindexに対してStringの挿入をして、その後テキストを表示し直してくれます。(バッキングストアなどについて気になる場合は、TextKitをご参照ください)

@objc func newLine(sender: UIKeyCommand) {
    // プログラム経由のテキスト更新はデリゲートメソッドは呼ばれない
    insertText("\n")
}

プログラム経由でテキスト更新をした場合にはデリゲートメソッドは呼ばれないことに注意してください。

まとめ

以上のような2つのポイントを抑えて実装することで、Shift + Enterでの改行操作が実現可能となります。

もしもっとスマートな実装方法などご存知の場合は、ご教授頂けると助かります。