メインコンテンツにスキップ

Building <InputAccessoryView> For React Native

·6 分間の読書
Peter Argany
Facebook ソフトウェアエンジニア

モチベーション

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> を所有しているのは誰でしょうか?UITextViewUITextField 自身が再び所有できますか?テキスト入力が入力アクセサリビューの内部にあります。これは循環依存のように思えます。この問題を解決するだけでも、別のブログ記事 になります。ネタバレ: 所有者は、私たちが手動で becomeFirstResponder にするよう指示した汎用 UIView サブクラスです。

APIデザイン

<InputAccessoryView> が何であるか、どのように使用したいのかがわかりました。次のステップは、両方のユースケースに意味があり、<TextInput> などの既存の React Native コンポーネントと適切に機能する API を設計することです。

キーボードツールバーの場合、考慮したい点がいくつかあります。

  1. 汎用 React Native ビュー階層を <InputAccessoryView> に取り出すことができるようにしたい。
  2. この汎用で独立したビュー階層がタッチを受け入れ、アプリケーションの状態を操作できるようにしたい。
  3. <InputAccessoryView> を特定の <TextInput> にリンクしたい。
  4. コードを複製せずに、<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リリースでご利用いただけます。

快適なキーボード操作を:)