Swiftで日本語入力できるキーボードアプリを作った

日本語入力のキーボードアプリ**azooKey**を作り、12月頭から[App Store](https://apps.apple.com/app/id1542709230)で公開しています。iOS向けキーボードアプリを作る上でのハードルや、かな漢字変換の実装の話を書きます。

# 概要

**azooKey**(*あずーきー*)は自作したiOS向けのキーボードアプリです。[統計的かな漢字変換システム Mozc](https://anlp.jp/proceedings/annual_meeting/2011/pdf_dir/C4-3.pdf)を参考にしながらかな漢字変換をSwiftで書き、長らく標準キーボードに欲しいと思っていたカスタマイズ機能をつけて公開しました。

## 構成
[統計的かな漢字変換システム Mozc](https://anlp.jp/proceedings/annual_meeting/2011/pdf_dir/C4-3.pdf)を参考にして、辞書や変換機能を作成しました。

* 使用言語: Swift, Python
* ライブラリ/ツール: SwiftUI, MeCab(辞書はIPAdic/NEologd)

大きく「辞書」「変換」「UI」の三点に分割して考えながら作りました。それぞれに割いた時間は3:4:3くらいです。

変換は全てSwiftで書きました。現代のかな漢字変換としてはベーシックな連接コスト+単語コスト+αを最小化するViterbiアルゴリズムを使っています。

変換にはかな文字列と変換先の単語を対応づける「辞書」が必要になります。Python+MeCab (NEologd)で作ったものをSwiftから呼び出して使いました。

UIはほぼ全てSwiftUIでの実装です。LazyStackやScrollViewReaderなどを使いたかったため、iOS14以上を端末の要件としています。

## カスタムキーボード

iOSにおける「カスタムキーボード」というのは、標準搭載されるキーボードに対してサードパーティーの作成したキーボードを指す言葉です。SimejiやGboardなどがこれにあたります。Keyboard Extensionと呼ぶこともあります。

カスタムキーボードはそれ単体で公開することはできず、必ず「**収容アプリ**」と呼ばれるアプリケーションに同梱して配信する必要があります。多くの場合この収容アプリが設定アプリとしての機能も担います。

カスタムキーボードはその私秘性のため、強い制限がかけられます。具体的には例えば以下の動作がOSによって**禁止(無効化)**されます。

* 外部へのアクセス
  * このため、外部サーバに入力データを送信したり、逆にAPIを叩いて情報を得たりすることはできません。
* 収容アプリとの共有ディレクトリへの書き込み
  * 共有ディレクトリへのアクセスはRead Onlyになります。こうしないとキーボードが共有ディレクトリに保存した内容を収容アプリが送信できてしまうからです。
* そのほか数々の制限
  * 3Dタッチのフィードバックを返せない
  * 位置情報は取れない
  * メモリ制限
  * UIPasteboardにアクセスできない

しかし発展的な機能を提供しようとする場合、これらの制限が障害になることがあります。そこでiOSではこれらの制限を解除する「**フルアクセス**」という機能があります。これにはユーザの許可が必要になりますが、これを用いることで「キーボード上でのWeb検索(Gboard)」「通信により端末外で重く時間のかかる計算を行い、より精度の良い変換候補を返す(Simejiなど)」のような機能を作ることもできます。

ただし、azooKeyは今のところフルアクセスを必要としていません。フルアクセスが必要になるほど高度な機能を提供していないからです。

## かな漢字変換の基本戦略

かな漢字変換とはかな文字列を対応する読みをもつ日本語の文字列に変換する処理です。azooKeyは連接コストと単語コストに基づいて変換する仕組みをとっています。

この場合、変換に用いる辞書は、実装にもよりますが、次のようなデータを含みます。

```
読み	単語	単語クラス	コスト
```

例えば

```
さる	去る	ラ行五段動詞終止形	800
さる	猿	名詞	1000
```

のような具合です。コストは小さい方が良いので、「さる」に対応する語が辞書にこの二つしかなければ「去る」がより正しい変換候補だと判断されます。このコストは「**単語コスト**」みたいに呼びます。

しかしこれだけだとあまり良い変換結果は得られません。例えば「さるがいる」を変換しようとすると「去るが居る」になってしまいます。そこで単語1つ1つのコストに加えて、単語クラス(≒品詞)を考慮することにします。例えば「名詞の後には助詞が来やすい」「動詞の未然形の後には助動詞が来やすい」のようなルールを考え、これを数値的に表現して「**連接コスト**」とします。考えられる全ての単語の境界でこのコストを加えていきます(つまりクラスbigramです)。

例えば「ラ行五段動詞終止形→助詞」の連接コストが1000であるのに対し、「名詞→助詞」の連接コストが「100」であれば、トータルで「猿が居る」が優先されることになります。このように考えられる全ての候補でこういう計算をしていけば最も良い変換候補が求まります。実用的にはこの方法だと死ぬほど時間がかかるので、動的計画法を使って高速化します。この部分の説明は省略しますが、枝刈りを賢く行うことで高速に動作させられます。

azooKeyは大筋この方式でかな漢字変換を実現しています
# 辞書

かな漢字変換を実現するため、品詞の情報と単語コストを持った単語リストが必要になりました。

MeCabの辞書はそもそも解析のため単語コストや連接コストのデータを持っているはずですが、学習対象をコントロールしたかったので自前でデータを用意した上で形態素解析してもらい、そこから必要な値を取得する、という方法を取りました。ただ、NEologdもIPAdicも形態素解析目的の辞書であってかな漢字変換用の辞書ではないので、これを使ってかな漢字変換用の辞書を作るのにいろいろ苦労しました。

## 生成

用意したテキストデータに前処理をかけた上でMeCabで解析し、単語コスト、品詞同士の連接コスト、その他諸々の値を取得します。それを使えるデータにまとめて合わせて行くと辞書が出来ます。

### 活用形生成

動詞・形容詞など一部の活用語は、辞書の生成時に活用形を自動で生成して辞書に追加しています。国語文法の話に一時期どハマりしていたのが使えて楽しかったです。

PythonもSwiftも変数名に日本語を使うことができます。このおかげで活用形の生成コードがめちゃくちゃ読みやすくなります。

```Swift
let 未然形 = (単語: 語幹 + 行の名前.あ段, 読み: 語幹読み + 行の名前.あ段)
let 連用形 = (単語: 語幹 + 行の名前.い段, 読み: 語幹読み + 行の名前.い段)
let 仮定形 = (単語: 語幹 + 行の名前.え段, 読み: 語幹読み + 行の名前.え段)
```

こんな感じです。

## LOUDS

噂によるとKeyboard Extensionは30MB以上のメモリを使ってはいけないらしいのですが、私のデバッグ環境でクラッシュするのは66MBでした。おそらく端末にもよるのではないでしょうか。公式の記述は見つけられませんでした。

辞書のエントリー数はおおよそ30万件です。パフォーマンスのためにキャッシュを取ろうとするとすぐにメモリを使い切ってしまいます。そこで省メモリな読み込みのためにLOUDSを利用しました。これのおかげでazooKeyのメモリ消費量は今でも20MB代に抑えられています。

Swiftでビット演算するのはほとんど初めてでしたが、どうにか実装しました。LOUDSの実装にあたってはこちらの記事にとてもお世話になりました。

* [｜Swiftでバイナリ🔟🔟🔟を扱う？！＞🐧　 - Qiita](https://qiita.com/st43/items/f58e764ae86d2963fc66)
* [簡潔データ構造 LOUDS の解説（全12回、練習問題付き）](https://takeda25.hatenablog.jp/entry/20120421/1335019644)

アプリ内の「オープンソース」の項目を見る限り、Simejiは[marisa-trie](https://github.com/s-yata/marisa-trie)を使っているようです。SimejiやFlickといった大手のキーボードはエントリーが100万超らしいですが、どうやって実現しているのかとても気になります。
## 難題

### 顔文字辞書(半解決)

顔文字と検索すれば顔文字をまとめたサイトは無数に見つかります。そうしたサイトはしばしば「IME向けのユーザ辞書」を配布してくれていますから、埋め込み可能な顔文字辞書も当然あるだろうと探し始めました。が、本当に見つからないのです。

現在は辛うじて見つかったMITライセンスの以下の2つのリポジトリを利用させていただいています。感謝申し上げます。

* [Kaomojitoka to Google IME Dictionary](https://github.com/nikukyugamer/kaomojitoka-to-google-ime-dictionary)
* [Kaomojic](https://github.com/mika-f/kaomojic)

なお、絵文字辞書については次のリポジトリを全面的に利用させていただいています。こちらも感謝申し上げます。

* [Emoji-IME-Dictionary](https://github.com/peaceiris/emoji-ime-dictionary)

### 同形異音語(半解決)

NEologdは「日本」を「にっぽん」と読みます。どの状況でもこう読むため、当初何も考えずに作った辞書は「にほん」を変換できませんでした。振り仮名候補のデータセットを用いて「日本」の出現頻度を「にっぽん」「にほん」「ひのもと」のような複数の読みの間で共有するようにしてみましたが、今度は「あした」で「朝」が「明日」よりも高く評価されてしまうなど困った副作用が出てしまいます。

そこでWikipedia APIに漢字と読みをセットで投げ、ヒット数で出現頻度を配分する、という荒技を採用しました。検索アルゴリズムに依存する点で雑なやり方ですが、そもそも「あした」と「朝」の副作用のような例は滅多に起こるものではないのでここが多少雑であっても大きな影響はありません。

最後に立ちはだかった問題は「包含関係にある読み」です。「夜 よ」は「夜 よる」の検索結果を必ず含むのでどうやっても「よ>>よる」と配分されてしまいます。これはどうしようもないため、このようなパターンでは別の方法を採用しています。

同形異音語の分配比はそう変わるものではないので、このような場合では手動で比を設定してしまえば問題なさそうですが、一方で何の根拠もない値に設定し直すのも主観的すぎる気がして躊躇っているところです。

余談ですが、現バージョンのNEologdは明倫養賢堂に「ようけんどう」とルビを振っています。従ってこれをそのまま使うと「ようけんどう」で「明倫養賢堂」と変換されますが、まさにSimejiがその挙動を示します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638786/452d3e4a-e267-6368-5a62-18dccb6599be.jpeg" width="80%">

この種の誤りは「光GENJI(ルビ: いち)」など、他にもいくつか見られました。どういう形かわかりませんがSimejiもNEologdから取得したデータを使っているのではないかと思います。

# 変換

変換は全てSwiftで書きました。基本的な方法は「戦略」に述べた通りです。

もともとかな漢字変換を書きたくて始めたプロジェクトだったのでそこまで苦痛ではなかったのですが、これまでパフォーマンスが決定的なプログラムを書いた経験がなかったため、とにかくパフォーマンスの改善に苦労しました。精度も速度も、今でも標準キーボードには全く及びませんが、改善を続けていきたいと思っています。

## 入力丸ごと変換

「とけいをもったおとこ」に対し「時計を持った男」のように全体を変換します。かな漢字変換の最も基本的な形です。

## 先頭文節変換

「とけいをもったおとこ」に対し「時計を」のように最初の文節を変換します。かな漢字変換における「文節」という単位は、必ずしも日本語文法の用語とは一致しないのですが、基本的には「連続する品詞」を見れば文節を決めることができます。例えば「助詞→動詞」の順番で連続していた場合、この間には区切りを想定します。

azooKeyでは文全体を変換した結果にこの方法を適用して最初の文節を取得し、候補に加えています。

## 先頭単語変換

「とけいをもったおとこ」に対し「時計」のように最初の単語を変換します。

azooKeyではラティス構築時に先頭から一致する単語を全て列挙しているため、そのデータを候補に加えています。

## 予測変換

### 末尾ヒント

「とけいをもったおと」に対し「時計を持った弟」「時計を持った男」のように、末尾の文字に一致する部分を含む単語を予測して変換します。取り出してきた単語を付け加えた上でコストを再計算し、良さそうなものを候補に加えています。

### 無ヒント(未完成)

「とけいをもったおとこ」に対し「時計を持った男**の**」のように一致部分がない単語を予測して追加します。ただ、日本語としての妥当性の評価が非常にシビアなため、現在は機能させていません。

## 入力誤り訂正

「たんしようひ」に対して「誕生日」のように、濁点忘れ、小書き忘れなどを自動で補います。これが効きすぎるとイライラするのでバランスがなかなか難しいところです。

azooKeyの誤り訂正は事前に誤りうる文字列のペアを用意しておき、それを組み合わせて正しい文字列にする、という方法で行っています。例えば入力「た」に対しては「だ」が誤りの候補として考えられますし、入力「ts」に対しては「ta=た」が誤りの候補として考えられています。このままだと単に「た」と「だ」の区別がつかないキーボードになってしまうので、誤りに対して減点を課すことでバランスをとっています。

ちなみにiOSの標準キーボードの誤り訂正はこれとは全く異なる方法で行われているようです。同じ入力内容であっても訂正候補は異なっていることがあり、おそらく「タップの位置」「入力間の待機時間」などの操作面の要素を考慮しつつ決められているものと思われます。こちらの方が的確な誤り訂正ができそうですが、SwiftUIの特性も相俟って実装難易度が段違いです。

### ローマ字入力における誤り訂正

「ts->ta」のような誤り候補を単純に適用すれば良いように思われますが、「カーソル移動」を考慮するとこれが地獄になります。

例えば「しりょく(siryoku)」を考えます。このときユーザがカーソルを移動して「しり|ょく」の状態となったとします。このときユーザが1文字削除して「し|ょく」になったらどうなるでしょうか。私の実装上誤り訂正は内部状態であるローマ字列を前提としていますが、今回の場合消された「り」に対応するローマ字列というものが存在しません。すると誤り訂正ができなくなってしまいます。

azooKeyではこのような動作において内部状態である「siryoku」を「siょku」に置き換えることでそのほかの部分の誤り訂正を維持することを可能にしています。これが本当に辛い実装でした。

ちなみに、標準ローマ字入力キーボードも、入力されたローマ字列を意識している点は一緒なのですが、カーソル移動をした場合の挙動はなかなか奇妙です。

* 「じょ(jo)」に対して「じ|ょ」とカーソル移動した上で「a」を打つと、表示は「じょあ(joa)」となります。jとoの間の文字は存在しないので追い出された形です。
* 「はしった(hasitta)」に対して「はしっ|た」とカーソル移動した上で「a」を打つと、表示は「はしたた(hasitata)」になります。判定上1つ目の「t」が「っ」とみなされているようです。
* 「こんわく(konwaku)」に対して「こん|わく」とカーソル移動したうえで「a」を打つと、表示は「こなわく」になります。この場合は「n」が「ん」とみなされています。

実のところフリックでも小書き・濁点・半濁点キーを真剣に取り扱うとこのような問題が生じますが、こちらは真剣に取り扱わなくてもまあまあ訂正できるのでそのようにしています。一方ローマ字はパフォーマンス上のオーバーヘッドになる程に誤り訂正が難しい上、得られるメリットは大して大きくない、という状況です。カーソルが移動されて入力/削除されたら誤り訂正を切ってしまった方がいいのかな、と少し悩んでいます。

### 打ち漏れ訂正(未実装)

先日ジャストシステムがiOS版ATOKへの実装を発表していて感動した機能です。今のところ実装方法の見当もつきません。

## 再変換(未実装)

「時計を盛った男」に対して「とけいをもったおとこ」とルビを取得し、より良い候補「時計を持った男」があればそれへの変換を返します。

iOSの標準キーボードでは、10文字までなら任意の文章から再変換することができます。例えば「紹介」を選択すると「商会」「蒋介石」「紹介して」などが候補に上がってきましたが、これのことです。やればできるような気もするのですが、慎重な実装が必要になりそうなので今のところ保留しています。

## 学習機能

入力履歴をキーボード内に保存して反映する、というシンプルな方式でやっています。最低限に留めていますが、もう少し改良が必要だと感じています。なお、入力履歴は全て端末内のみで利用されます。

# UIの実装

UIはSwiftUIで作りました。AutoLayoutやDelegateを大量に書かなくて済んで大変楽でしたし、今後も使いたいと感じます。ListやForEachなどを書くのも圧倒的に楽です。UIKitで本格的な開発を行った経験はないので比較はできませんが、TableViewやStackViewの実装が苦痛だった記憶を思い返すと、おそらくSwiftUIがなければazooKeyは完成しなかったんじゃないかと思います。

逆に難しかったところとしては、情報の少なさがありました。SwiftUIの発表からまだ1年弱しか経っていないこともあり、SwiftUIでカスタムキーボードを作るという情報は探してもほとんど見つかりません。開発の初期には「Keyboard Extensionだからうまくいかないのか、そもそも何か間違っているのか」というような問題が多く出てきました。例えばContextMenuはiOS13のカスタムキーボードでは不安定で、日々異常な挙動を見せていましたが、iOS14になると直りました。OSが間違っていたようです。

「一度も回らないfor文があるとメモリリークするが、for文を消すとリークがなくなる」「iOS14.0でリークせず、14.1でリークして、14.2でリークしない」といったお化けのようなバグが起こり、これも悩みの種でした。もちろん、これらの不安定な挙動にはSwiftUIへの無知も起因する部分があると思います。収容アプリの方はフルSwiftUIですが、今のところメモリリークは一度も見られません。

gestureの取得も困難でした。最初から用意されている「Drag」「Rotation」「Scale」のような基本的なジェスチャーの使い勝手は素晴らしいのですが、用意されていないことをやろうとすると途端にどうしようもなくなります。例えばLongPressGestureを使うと長押しするまでは取得できても指を離したタイミングを取得できません。あるいはDragGestureを使うと指を動かさないと全く反応が起こらなくなります。azooKeyのキーでは巨大なDragGestureの中で長押しやフリックの判定をしています。この辺りはSwiftUIの進化に期待したいところです。

そのほか、TextFieldのEnterKeyTypeが設定できない、リストの色を変えられない、最前面に配置するのがあり得ないほど困難、など、いくつか厳しいものがあったと記憶しています。総じてSwiftUIは「難しいことが簡単になったが、簡単なことが難しくなった」という印象です。ちょうど昨年今ごろの[SwiftUIアプリ開発実践ポイント](https://qiita.com/tattn/items/920f17868ee3f97ea17d)という記事で

>* 細かなUIの動きまでアプリの仕様に沿って実装する必要がある場合はかなり難しい
>* アプリの仕様をSwiftUIが得意としている仕様に柔軟に変更できる場合は採用しても良い

と書いてありましたが、この状態が今年も継続しています。

ただ、Keyboard Extensionの開発のためのツールはそもそもSwiftUIで作ることを想定して作られていません。入り口がUIInputViewControllerに準拠していないと、そもそも入力や削除などのキーボードとしての機能を実装できません。また地球儀マークを長押しして出るキーボード選択画面はいまだにSelectorを経由しないと出せません。SwiftUIでSelectorを扱うのが難しく、結局viewControllerにビューを作らせた上でkeyboardのビューに渡しています。

WidgetKitはSwiftUIしか受け付けないと聞いたので、そのうちKeyboard ExtensionもSwiftUI対応することを期待しています。

## 内部の制御

### UITextDocumentProxy

Keyboard ExtensionのUIを作る上で理解しなければいけないことは多くなく、この「UITextDocumentProxy」のいくつかのツールが利用できればほぼOKです。そのツールというのは

* `insertText`: テキストの入力
* `deleteBackward`: テキストの削除
* `adjustTextPosition`: カーソルの移動
* `documentContextBeforeInput`,`documentContextAfterInput`: カーソル前後のテキストの取得
* `selectedText`: 選択されているテキストの取得
* (`setMarkedText`, `unmarkText`): ハイライトされたテキストの入力・ハイライトの解除

です。このほかにキーボードの設定を取得するいくつかのプロパティがあります。

注意点がいくつかあります。

まず`adjustTextPosition`が微妙です。ドキュメントには「引数には移動したい文字数を指定してね」と書いてありますが、これを鵜呑みにして「文字数(`String.count`の意味で)」を指定しているとサロゲートペアや結合文字に対応できずバグります。正しくは「文字数(`String.utf16.count`の意味で)」なので、このメソッドを呼ぶ前に移動したい方向の文字を取得して`String.utf16.count`を計算しておく必要があります。

`documentContextBeforeInput`と`documentContextAfterInput`、及び`selectedText`も改行周りの挙動が奇妙で混乱必至です。特に`selectedText`は「3行以上かつ改行含めて65文字以上選択していると両端2行をくっつけて返してくる」というどこにも書いていないクールな仕様があります。動作自体は意図的にそうなっているようにも見えるのですが、ドキュメント等に記載のない不思議な挙動なので不具合としてフィードバックを送っています。

`setMarkedText`と`unmarkText`はカッコで囲みました。この二つは挙動が奇妙を通り越してぶっ壊れているため使っていません。これも不具合としてフィードバックを送っています。早めに修正されて欲しいところです。

この辺りの奇妙な挙動はそれぞれ以下の記事で書いているので、もし興味があったらご覧ください。

* [iOS Keyboard ExtensionでadjustTextPositionするときのお作法 - Qiita](https://qiita.com/En3_HCl/items/0f696fd8853a00b3b5c0)
* [UITextDocumentProxyのselectedText, documentContextBeforeInput, documentContextAfterInputの仕様について - Qiita](https://qiita.com/En3_HCl/items/37f74b47580d0f56da5d)
* [UITextDocumentProxyのsetMarkedTextを(まだ)使ってはいけない - Qiita](https://qiita.com/En3_HCl/items/1f1a327ce3050aad47b5)

そのほか、UIInputViewControllerの提供する`textWillChange`,`textDidChange`の引数が常に`nil`なこと、`selectionWillChange`,`selectionDidChange`が呼ばれた試しがないことなどいくつかポイントはありますが、それほど大きくないので端折ります。

WidgetKitのようにライブラリの思想を理解する大変さはないので、その点は心配しなくて大丈夫です。

### 入力追跡

キーボードの実装にあたっては、キーの入力を自力で追跡する必要があります。つまりどこからが現在の入力範囲なのかを把握しておかなければいけない、ということです。

単に入力と削除だけを想定する場合はシンプルなのですが、カーソル移動を考慮するとやはり途端に難しくなります。しかもカーソル移動はキーボードを介さず行われうるため、これを検知して現在のカーソル位置などを追跡する必要が出てきます。

この部分は以下の記事で具体的な実装例を紹介しています。少々面倒ですが、実装する上では必要になってきます。

* [iOS Keyboard Extensionでキーボードを介さない操作を検知する - Qiita](https://qiita.com/En3_HCl/items/6111815ff5844cc59b38)

この入力追跡キットはそのうちライブラリにでもしてみたいです。

### 文字をマークする(未解決)

標準キーボードで文字を入力すると、入力中の文字が薄い青(黄色)でマークされます。どこまで入力しているのか分かりやすくてとても良いのですが、azooKeyはそれを実装していません。

iOS13以降は`setMarkedText`が提供されていて、これを用いれば入力中の文字をマークすることが可能です。ところが挙動がとても微妙です。「不安定ですがいいですか？」というわけにはいかないので見送りました。もう少し挙動が安定したら実装しようかと思います。

## カスタマイズ

### カスタムキー

目玉機能の一つとして実装したのがカスタムキー機能です。日本語話者が文章を入力する際、ひらがな・漢字を使うところまでは同じでも、感情表現などに使う記号はバラバラです。例えば「笑い」の表現には「笑」「わら」「w」「(笑)」「☺️」などの多様性があります。言い淀む(溜める)「・・・」にしても、「...」「・・・」「。。。」「、、、」「……」など、人によってどれが好き、どれが嫌い、というような感覚は違います。カスタムキ─機能はこのような個人個人の好みに合わせてキ─ボ─ドをカスタマイズできるようにします。

私が高校生の時、地味に困ったのが標準のフリックキーボードで「ゐ」「ゑ」が打てないことでした。古文について話したい時この問題は致命的です。もちろん「い」「え」で変換すればいいのですが、面倒でした。ところがカスタムキーがあれば、そこに「ゐ」「ゑ」を登録しておくことでいつでも入力できるようになります。

カスタムキー機能は現在、フリック入力では「小゛゜」「、。！？」キー、ローマ字入力では数字タブに割り当てています。フリック入力ではたった6文字になってしまうのですが、それでも私の入力効率は格段に上がっています。普段使う記号を登録して使えるのはとても便利です。

### ユーザ辞書

azooKeyでは絵文字・顔文字をそれぞれ任意選択としています。どちらも使う人は使うけれど使わない人は全く使わない、という類のものなので、自分の好きなように設定できた方が嬉しいです。この機能はユーザ辞書の仕組みを用いて実現しています。

これに加えてユーザが好きな単語を登録するユーザ辞書機能も搭載しています。ポイントはいくつかあるのですが、動詞の活用形生成をどう実現するかでかなり悩みました。

例えば「切り替える」ことを意味する「トグる」という動詞を追加したいとします。この時当然「トグりたい」「トグらない」「トグろう」とも打ちたいですよね。このためには活用形を生成する必要があります。しかしユーザが選んだ単語が「動詞かどうか」というのは自明ではありませんし、「五段活用かどうか」も自明ではありません。ただ「五段活用の動詞である」というチェック項目があったとして、押す人は少なそうですよね。

そこでazooKeyでは「それらしい」単語に対して、「『トグらない』と言える」のようなチェック項目を追加します。この項目がそのまま五段活用の動詞かどうかを判定する役目を持つことで、「トグる」を追加しただけで他の全活用形も自動で生成する機能を実現しています。

### 文字の拡大表示

家族から要望を受けて実装した機能です。変換候補を長押しして呼び出すことができます。

話を聞いたときは「それなにに使うの？」というのが正直な感想だったんですが、作ってみるとこれがめちゃくちゃ便利だったので採用になりました。例えば難しい漢字の名前を書きたい場面では拡大したほうが字の構造がわかりやすいですし、「それってどんな字書くの？」と聞かれたときに口で説明せずともパッと見せられるようになります。**地味ですが、めちゃくちゃ便利です**。ぜひ使ってみてください。

## 文頭まで削除

Simejiと早手キーボードにあった機能です。とても便利なのでフリックの方に実装しました。ローマ字入力にも実装したいところですが、どこにおくべきかわからず放置しています。

実装は単純で、次のようにやっています。

```swift
func deleteToHead(){
    var leftSideText = self.textDocumentProxy.documentContextBeforeInput ?? ""
    var count = 0
    while let last = leftSideText.last{
        if ["、","。","！","？",".",","].contains(last){
            if count == 0{
                count = 1
            }
            break
        }
        leftSideText.removeLast()
        count += 1
    }
    //削除を実行する
    (0..<count).forEach{_ in
        self.textDocumentProxy.deleteBackward()
    }
}
```

`documentContextBeforeInput`はそもそも改行から前の部分を取得できないので、改行を考慮する必要はありません。勝手に止まります。

## ~~文字数・単語数カウント~~

~~実装コストは低いですがこれも是非欲しかった機能でした。~~

~~受験勉強をしていた頃、出先では英作文や国語の記述問題をスマホに打ち込むことがありました。しかし字数や単語数はわざわざカウントアプリにコピペして数えるしかありませんでした。azooKeyではカウンタ機能をキーボードに統合しているため、いつでも簡単に文字数・単語数を数えることができます。~~

~~ただ、前述の`selectedText`は「3行以上かつ改行含めて65文字以上選択していると正しい選択範囲を返さない」という挙動のため、複数行を選択した場合の文字数カウントは切っています。~~

選択されている部分が正しく得られないという解決不能な不具合のため機能を削除しました。詳しい説明は下記記事で行っています。

* [UITextDocumentProxyのselectedText, documentContextBeforeInput, documentContextAfterInputの仕様について - Qiita](https://qiita.com/En3_HCl/items/37f74b47580d0f56da5d)


# デザイン

個人で作っているのでデザインも個人でする必要がありました。

アイコンとロゴについては幸いGlyphs Miniというフォント作成ソフトを持っていたのでこれを使って作成し、アイコンフォントとして書き出したうえで利用しています。シンプルでかわいいアイコンができました。豆としての小豆をイメージして作ったのですが、どことなくニコニコしているように見えるところが気に入っています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638786/4f84e599-b727-2065-21d6-8ac833e6703a.png" width="130px">

アプリのスクリーンショット(App StoreのPR画像)は苦戦しました。多くのアプリはスマホ画面上側を切り抜いて使っていますが、キーボードではスマホ画面下側が重要になるので、そもそも違和感のあるデザインになってしまいます。大苦戦した挙句「まとまってはいるが、ダサい」という微妙なポイントで止めてしまいました。どこかのタイミングでアップデートしたいところです。

キーボードのデザインも悩ましいポイントでした。結論としてライトモードでは標準のキーボードよりも若干フラットで薄め、ダークモードでは黒をメインにした濃いめのデザインにしています。

着せ替え機能も実装しました。

## Webページ

プライバシーポリシーの掲載のためサイトを用意しなければいけないのですが、流石になんのデザインもないものを出すのも忍びなかったため頑張りました。

Webの知識はHTML, 生js, CSSまの初歩止まりだったのでどうにもならず、後輩が使っていたVue.jsを使ってみました。全体を理解しなくても作れる程度に静的なページだったことが幸い(学習機会という点では災い)し、Vue.jsをちょっと便利なHTMLと認識したレベルで止まってしまいました。そのうちちゃんと勉強したいと思います。CSSを書くのは苦痛でした。

Netlify?? npm?? Deploy????となりながらどうにかサイトは作れました。後からGithubにファイルあげればいいだけだと聞きましたが、多少でも勉強にはなったのでよかったです。

# 感想

azooKeyは大学の授業でかな漢字変換のアルゴリズムを知ったのをきっかけに8月末から作り始めました。カスタムキーボード自体は2年前からたびたび手を出していたのですが、公開できるまでに仕上げるのは初めてだったので、ひとまず形になってよかったです。

## iOSの日本語対応キーボード開発

個人で作業することによるものもありますが、なかなか大変だと思います。

まず、非常に高い水準が要求されています。標準キーボードは非常に高機能で使いやすいため、最低でも同レベルの変換能力が求められるか、それをカバーする操作性が必要になります。azooKeyは私自身の利用では標準の置き換えに十分なのですが、まだ全ての人にとってそうだという水準ではありません。例えば最新の機種であっても、入力の速い友人は「若干もたつく」と言っていました。

広告による収益化は困難です。キーボードを開くたびに広告が表示されるのに使い続けたいと思えるほどいいキーボードを作れるなら別ですが、キーボード上に広告を出すのは難しいでしょう。そうなると収容アプリに広告を出し、なるべく収容アプリにユーザを閉じ込める、というのが広告による収益化の方針になりそうですが、キーボード以外のことにかなりのリソースを割く必要が生じます。買い切りや定額制のキーボードなら収益化可能でしょうか。大多数のiOSユーザは標準キーボードを使っています。その上すでにカスタムキーボードには6年の歴史があります。三大無料キーボードであるSimeji、Flick、Gboardがカスタムキーボードを検討するユーザの大部分を持っていき、有料ではSimeji pro、ATOK、片手キーボード、WordLightなど高性能なキーボードがすでにたくさんあります。レッドオーシャンです。azooKeyも今後収益化できるとはあまり思えません。

とはいえ、自分の欲しい機能を実装してどこでも使えるのは最高です。今使っているスマホキーボードに不満がある方は、ぜひ開発にチャレンジされてはいかがでしょうか。

## 今後の方向性

今最も必要なのはやはり基礎的な変換機能の向上だと考えています。変換能力は標準キーボードに遠く及びませんし、プロセッサが古めの端末では動作が重いのが大問題です。辞書・変換アルゴリズムの双方を見直しつつ、もっと精度をあげ、ストレスなく操作できることを目指したいです。

と同時に、「自由自在なキーボードアプリ」と銘打つ以上、カスタム機能の幅を広げていきたいと思っています。フリック入力はもっともっとカスタマイズできるようにしたいですし、ローマ字カスタムキーの登録画面はお世辞にもわかりやすいとは言えないので、視覚的にわかりやすいUIを検討中です。

今後も開発を続けていきますので、ぜひ試していただければ嬉しいです！

開発・更新情報を共有します↓

https://twitter.com/azooKey_dev

2年経った後日談↓

https://zenn.dev/azookey/articles/abeeb27c248e22

# 参考リンク

以下のリンクは作成の上で特に参考にしたものです。この他にも多数の記事に助けられました。

* [iOSカスタムキーボードの作り方 - Qiita](https://qiita.com/Kyome/items/4257de4e33d3647085e6)
* [統計的かな漢字変換システム Mozc (工藤拓, 小松弘幸, 花岡俊行, 向井淳, 田畑悠介)](https://anlp.jp/proceedings/annual_meeting/2011/pdf_dir/C4-3.pdf)
* [Custom Keyboards - Extensions - iOS - Human Interface Guidelines - Apple Developer](https://developer.apple.com/design/human-interface-guidelines/ios/extensions/custom-keyboards/)