Building <InputAccessoryView> For React Native
モチベーション
3 年前、インプットアクセサリビューを React Native でサポートするための GitHub の課題が開かれました。

それからの数年間、無数の「+1」、さまざまな回避策がありましたが、この問題に関して RN に実質的な変更が加えられることはありませんでした。今日まで、iOS から始まり、ネイティブインプットアクセサリビューにアクセスするための API を公開しており、その作成方法を共有することに興奮しています。
背景
インプットアクセサリビューとはどういうものでしょうか。Apple の開発者向けドキュメントを見ると、受信者が最初のリスポンダーになった場合にシステムキーボードの上部に固定できるカスタムビューであることがわかります。UIResponder
から継承するものは何でも、.inputAccessoryView
プロパティを読み取り書き込みとして再宣言し、ここでカスタムビューを管理できます。レスポンダーインフラストラクチャはビューをマウントし、システムキーボードと同期した状態に保ちます。キーボードを非表示にするドラッグやタップなどのジェスチャーは、フレームワークレベルでインプットアクセサリビューに適用されます。これにより、iMessage や WhatsApp などのトップクラスのメッセージングアプリで不可欠な機能である、インタラクティブなキーボード非表示機能を備えたコンテンツを作成できます。
キーボードの上部にビューを固定するための一般的なユースケースは 2 つあります。1 つ目は、Facebook の作曲者背景ピッカーのような、キーボードツールバーを作成することです。

このシナリオでは、キーボードはテキスト入力フィールドに焦点を合わせており、インプットアクセサリビューは追加のキーボード機能を提供するために使用されます。この機能は、入力フィールドの種類によって異なります。マッピングアプリケーションではアドレスの候補であり、テキストエディターではリッチテキストの書式設定ツールにすることができます。
このシナリオで <InputAccessoryView>
を所有する Objective-C UIResponder は明確である必要があります。<TextInput>
が最初のリスポンダーになり、基盤では UITextView
または UITextField
のインスタンスになります。
一般的な 2 つ目のシナリオは、スティッキーテキスト入力です。

ここでは、テキスト入力が実際にインプットアクセサリビューの一部です。これはメッセージングアプリケーションで一般的に使用され、以前のメッセージのスレッドをスクロールしながらメッセージを作成できます。
この例の <InputAccessoryView>
を所有しているのは誰でしょうか?UITextView
や UITextField
自身が再び所有できますか?テキスト入力が入力アクセサリビューの内部にあります。これは循環依存のように思えます。この問題を解決するだけでも、別のブログ記事 になります。ネタバレ: 所有者は、私たちが手動で becomeFirstResponder にするよう指示した汎用 UIView
サブクラスです。
APIデザイン
<InputAccessoryView>
が何であるか、どのように使用したいのかがわかりました。次のステップは、両方のユースケースに意味があり、<TextInput>
などの既存の React Native コンポーネントと適切に機能する API を設計することです。
キーボードツールバーの場合、考慮したい点がいくつかあります。
- 汎用 React Native ビュー階層を
<InputAccessoryView>
に取り出すことができるようにしたい。 - この汎用で独立したビュー階層がタッチを受け入れ、アプリケーションの状態を操作できるようにしたい。
<InputAccessoryView>
を特定の<TextInput>
にリンクしたい。- コードを複製せずに、
<InputAccessoryView>
を複数のテキスト入力間で共有できるようにしたい。
React ポータル と同様の概念を使用して、1 番目を達成できます。この設計では、React Native ビューを「レスポンダーインフラストラクチャによって管理される UIView
階層にポータルします。React Native ビューは UIView としてレンダリングされるため、実際には非常に簡単です。オーバーライドするだけです。
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
そしてすべてのサブビューを新しい UIView 階層にパイプします。2 番目の場合、<InputAccessoryView>
に新しい RCTTouchHandler を設定します。状態の更新は、通常のイベントコールバックを使用して実現されます。3 番目と 4 番目で、.inputAccessoryView
コンポーネントの作成中にネイティブコードでアクセサリビュー UIView 階層を見つけるために nativeID フィールドを使用します。この関数は、基盤となるネイティブテキスト入力の .inputAccessoryView
プロパティを使用します。これにより、<InputAccessoryView>
が ObjC の実装で <TextInput>
に効果的にリンクされます。
スティッキーテキスト入力のサポート(シナリオ 2)を追加すると、制約がさらにいくつか追加されます。この設計では、入力アクセサリビューにテキスト入力の子があるため、nativeID 経由でリンクすることはできません。代わりに、汎用オフスクリーン UIView
の .inputAccessoryView
をネイティブ <InputAccessoryView>
階層に設定します。この汎用 UIView
に手動で先頭レスポンダーになるように指示すると、レスポンダーインフラストラクチャによって階層がマウントされます。この概念は、前述のブログ記事で詳しく説明されています。
落とし穴
もちろん、この API の構築中にすべてが順調に進んだわけではありません。以下に、発生したいくつかの落とし穴と、それらをどのように修正したかを示します。
このAPIを構築するための当初のアイデアでは、UIKeyboardWill(表示/非表示化/フレームの変更)イベントのNSNotificationCenter
をリッスンしていました。このパターンは、いくつかのオープンソースのライブラリや、Facebookアプリの一部に内部的に使用されています。残念ながら、UIKeyboardDidChangeFrame
イベントは、スワイプ時に<InputAccessoryView>
フレームを更新するタイミングで呼び出されていませんでした。さらに、このイベントではキーボードの高さの変更はキャプチャされません。これにより、次のように顕在化するクラスのバグが発生します

iPhone Xでは、テキストキーボードと絵文字キーボードの高さは異なります。キーボードイベントを使用してテキスト入力フレームを操作するほとんどのアプリケーションでは、上記のバグを修正する必要がありました。私たちの解決策は、.inputAccessoryView
プロパティを使用することでした。つまり、応答者のインフラストラクチャは次のようにフレームの更新を処理することを意味します。
私たちが遭遇したもう1つの厄介なバグは、iPhone Xのホームボタンを避けることでした。「AppleがsafeAreaLayoutGuideをまさにこの理由のために開発した。これは簡単だ!」と考えているかもしれません。私たちも同様に世間知らずでした。最初の問題は、ネイティブの<InputAccessoryView>
実装には、表示される瞬間までアンカーするウィンドウがないことです。問題ありません。そこで、-(BOOL)becomeFirstResponder
をオーバーライドして、レイアウト制約をそこで適用できます。これらの制約に従うとアクセサリービューは押し上げられますが、別のバグが発生します:
入力アクセサリービューはホームボタンを回避することに成功しますが、今度は安全でない領域の背後にあるコンテンツが表示されます。このレーダーに解決策があります。ネイティブの<InputAccessoryView>
階層を、safeAreaLayoutGuide
制約に準拠しないコンテナでラップしました。ネイティブコンテナは安全でない領域のコンテンツをカバーしますが、<InputAccessoryView>
は安全な領域の境界内に留まります。
使用例
<TextInput>
の状態をリセットするキーボードツールバーボタンを作成する例を示します。
class TextInputAccessoryViewExample extends React.Component<
{},
*,
> {
constructor(props) {
super(props);
this.state = {text: 'Placeholder Text'};
}
render() {
const inputAccessoryViewID = 'inputAccessoryView1';
return (
<View>
<TextInput
style={styles.default}
inputAccessoryViewID={inputAccessoryViewID}
onChangeText={text => this.setState({text})}
value={this.state.text}
/>
<InputAccessoryView nativeID={inputAccessoryViewID}>
<View style={{backgroundColor: 'white'}}>
<Button
onPress={() =>
this.setState({text: 'Placeholder Text'})
}
title="Reset Text"
/>
</View>
</InputAccessoryView>
</View>
);
}
}
リポジトリには、スティッキーテキスト入力の別の例があります。
いつ使えますか?
この機能実装の完全なコミットはこちらです。<InputAccessoryView>
は、今後のv0.55.0リリースでご利用いただけます。
快適なキーボード操作を:)