カスタムキーボードは様々な状況で表示されるという特徴から、普通のアプリ以上に堅牢なレイアウト処理を施す必要があるかと思います。(AutoLayoutが使えないのでなおさら)
しかも表示する状況によってビューのライフサイクルが変わってくることに気がつきました。
この例外的なビューのライフサイクルも意識したコードを書いていないと、
ユーザーに無様な姿をさらしてしまうことになりかねません。
この問題はぼく個人のただの動作チェック漏れかと思いきや、大御所キーボードも含め、いくつものキーボードで対処しきれていないことがわかりました。
カスタムキーボード開発者にとっての死角になってしまっているので、
僕が見つけた限りでご紹介したいと思います。
(起動パターンとありますが、厳密には起動・回転パターンです)
(実行環境はiOS9.3.1, iPad Air2)
1. キーボード切替直後にデバイスを回転させる
システムキーボードのグローブキーのタップと同時にデバイスを回転させ、カスタムキーボードのビューが正確にレイアウトされるかを確認します。
普通に回転させずに起動することと違うのは、
viewDidLoad()と、viewWillAppear()後のキーボードwidthが異なる点です。
viewDidLoad()ではランドスケープとして認識していたのに、viewWillAppear()後にポートレイトになっていた、という現象が発生します。
レイアウトはviewWillAppear()後に行うべきではありますが、viewWillAppear()後ですら、updateViewConstraints()内のキーボードwidthが異なっています。
物理的にはデバイスが回転したわけですが、viewWillTransitionToSize()メソッドも呼ばれません。
viewWillTransitionが呼ばれるタイミングのあとに、デバイスが回転したということなのでしょう。
通常の起動と違い、キーボードwidthまわりの変化が複雑になっていることは事実なので、動作チェックを行っておくのが無難だと思いました。
キーボードは即座に切り替わってしまうため、この状況を再現するのが難しいですが、
コツとしては、インストール直後の最初の起動は時間がかかる傾向があるため、そのタイミングでデバイスを回転させると再現しやすいと思います。
再現がうまくいくと、システムキーボードはこのスクショのように、歪んだレイアウトが一瞬表示されます(そしてカスタムキーボードに切り替わる)。
2. キーボードの長押し操作をしながらデバイスを回転させる
長押しジェスチャーを使っている人向けの内容です。
キーボード上のなんらかのviewを長押ししたまま、デバイスを回転させ、レイアウトが適切に実行されるか確認します。
僕のキーボードでは、あるviewに長押しジェスチャーで位置を調節する機能を割り当てているんですが、
長押しをしている間、viewWill/DidLayoutSubviews()が不要に呼び出されてしまうため、長押し中はそれを防ぐ対策をしていました。
しかし長押しのままデバイスを回転させると、レイアウト処理を書いたviewWill/DidLayoutSubviews()が実行されないため、適切にレイアウトされないことが発覚しました。
この動作チェックは手軽にできますね。
3. ShareエクステンションのEvernoteでノートブック一覧を一度開き、戻る
Safariや写真アプリの共有ボタンからEvernoteを選択し、ノートブックリストを表示させます。
そして前の画面に戻ります。
このときノートブックを切り替える必要はありません。
前の画面に戻ると同時にテキストフィールドにフォーカスされ、キーボードが立ち上がると思いますが、ここでちゃんと表示・レイアウトされるか確認します。
このパターンの問題は、viewWillAppear()直後にupdateViewConstraints()が呼ばれなくなることです。
下のログのオレンジ枠の囲みの部分ですが、
通常起動では、viewWillAppear()後にupdateViewConstraints()が複数回呼ばれ、キーボードの高さが適切になり、viewのレイアウトを開始、そしてviewDidAppear()が最後に呼ばれます。
しかしEvernoteエクステンションのノートブック一覧からの復帰では、
なぜかupdateViewConstraints()が呼ばれないままviewDidAppear()に至ります。
updateViewConstraints()が呼ばれないため、viewWill/DidLayoutSubviews()も呼ばれないのです。
そしてレイアウトが実行されないので、僕のキーボードではviewが何も表示されなくなりました。
PathInputというキーボードも同様ですし、
ATOKのテンキーでは、左右どちらか片方に寄っているべきなのに、横幅いっぱいのレイアウトになってしまいます。
僕は対策として、viewDidAppear()でframeが設定されていないことを検知し、
self.view.layoutIfNeeded()を実行するようにしています。
4. Shareエクステンション使用中にデバイスを回転させる
同じくShareエクステンションですが、今回はEvernoteに限りません。
Shareエクステンションを使用中にデバイスを回転させ、キーボードがちゃんとレイアウトされるか確認します。
このパターンの問題は、viewWillTransitionToSize()が呼ばれないことです。
このメソッドはデバイスの回転時に呼び出されるはずですが、なぜかShareエクステンションでは呼び出されません。
viewWillTransitionToSize()で回転レイアウトに関する処理を書いている場合、Shareエクステンションでの回転時にレイアウトがおかしくなる可能性があります。
例えばFleksyでは、ランドスケープでキーボードを起動し、ポートレイトへ回転させると、左右両端のキーが表示されなくなります。
Swypeですと、通常であれば回転とともにアニメーションで綺麗にレイアウトが変わるんですが、
Shareエクステンションのときはそのアニメーションが実行されず、
回転が完了したあとに
「ハッ(‘Ц°)!」
と気づいたかのように、パッとレイアウトが変わります。
おそらくビューサイクルが終了したときに、正しくレイアウトがなされているかをチェックする仕様になっているんだと思います。
5. Split View使用中、両アプリのフィールドを行き来してから境界線をいじる
手順は以下の通り。
【1. Split Viewにする】
【2. もう片方のアプリへフォーカスを移す】
【3. 中央の境界線をいじる】
【4. 再表示されたキーボードをチェックする】
ここで使用したFleksyでは、一番下の部分が隠れてしまっていますよね。
僕のキーボードはビューが表示されませんでした。
このパターンの問題は、viewDidLoad()が呼ばれずにviewWillAppear以降が実行されることです。
僕の場合、Disappear()でビューをremoveFromSuperviewし、viewDidLoad()で再生成するという仕様がこのパターンで非表示をまねいていました。
removeFromSuperviewをやめ、コントローラの解放による連鎖的なビューの解放に任せたところ、viewDidLoad()が呼ばれなくてもちゃんと表示されるようになりました。
メモリ的にもいまのところ特に問題はないようです。
ちなみにSwypeですと、フォーカスを変えるたびにキーボードが半分埋没してしまいます。
(ただし直後に正しいレイアウトへ戻ります)
6. Split View使用中、Safariのメッセージエクステンションを表示し、デバイスを回転させる
このパターンはおそらく最も局所的で、最も根の深い現象だと思います。
手順は以下の通り。
【1. Split Viewの状態でSafariのShareエクステンションのメッセージを起動する(ポートレイトで)】
【2. もう片方のアプリにいったんフォーカスを移す】
【3. 再度メッセージにフォーカスを移す】
【4. ランドスケープに回転させる】
回転させると、テキストフォーカスが強制的にキャンセルされ、
キーボードは隠れます。
【5. メッセージアプリのテキストフィールドをタップする】
再度キーボードを表示させ、ここでレイアウトを確認してみましょう。
ご覧のように、右側にスペースができてしまっています。
これはFleksyだけでなく、僕のキーボードはもちろん、ATOKもPathInputでも同様に発生します。
この現象の原因は
inputView(UIInputViewControllerのself.view)の横幅が更新されていないからです。
ランドスケープになったにも関わらず、キーボードそのもののwidthが変わっていないため、
レイアウトがポートレイトのまま更新されないんです。
しかもそのままポートレイトへ戻すと、追い打ちをかけるかのように、レイアウトはさらに圧縮されます。
下のログで分かるとおり、inputViewのframeのwidthは「768」のままですが、
UIScreen.mainScreen().bounds.size.widthの値は「1024」になっています。
で、僕はまだこの問題の対処ができていません。笑
この項目を書いているときにこの現象の確実な再現方法がわかったばかりなので、、、
この場合はmainScreenに従い、手動でinputViewのwidthを更新しないといけないんじゃないでしょうかね。
ちなみにデバッグのときに、Shareエクステンションのメールやメッセージを開いても
下の画像のように(null)と表示されて切り替えられなくなることがあります。
これではデバッグしようにもできないので、デバイスを再起動し、再度ビルドしましょう。
そうすればXcode使用中でも動作を確認できるようになります。
参考:【開発者・ユーザー向け】iOS9カスタムキーボードが起動しない問題と(null)への対処法
まとめ
メジャーなキーボードでも対策しきれていないパターンがいくつもありました。
おそらく最も確実な対処法は、Swypeのようにビューサイクルの最後に(または遅延で)レイアウトを再チェックすることだと思います。
Swypeはどのパターンでも最終的には正しいレイアウトに調整されました。
ただ、見た目に難あり、です。笑
一瞬おかしなレイアウトになったかと思えば、パッとレイアウトが整うその光景は見ていて気持ちのいいものではありません。
ガタガタっと騒がしくレイアウトしているようで、目にも優しくはないです。
(ATOKも同じような感じ)
個人的には厳しく動作チェックを行い、なるべくデフォルトの回転アニメーションに準ずるような美しいレイアウトを実現することがユーザーにとっては望ましいんじゃないかと思ってます。
今後のiOSのアップデートでも、共有エクステンションとSplit Viewに対しては特に目を光らせておいたほうがよさそうです。
ViewControllerのライフサイクルについてはこちらの記事が勉強になりました。(英語)
iOS UIViewController lifecycle