建置 <InputAccessoryView> For React Native
動機
三年前,有人在 GitHub 上提出 issue,要求支援 React Native 的輸入輔助視圖。

在接下來的幾年中,出現了無數的「+1」、各種變通方法,以及針對此 issue 的 RN 零具體變更 - 直到今天。從 iOS 開始,我們正在公開一個 API 以存取原生輸入輔助視圖,我們很高興分享我們是如何建置它的。
背景
輸入輔助視圖到底是什麼?閱讀 Apple 的開發人員文件,我們了解到它是一個自訂視圖,只要接收器成為第一響應者,就可以錨定到系統鍵盤的頂部。任何從 UIResponder
繼承的物件都可以將 .inputAccessoryView
屬性重新宣告為讀寫,並在此處管理自訂視圖。響應者基礎架構會掛載視圖,並使其與系統鍵盤保持同步。框架層級會將關閉鍵盤的手勢(例如拖曳或點擊)應用於輸入輔助視圖。這讓我們可以建置具有互動式鍵盤關閉功能的內容,這是 iMessage 和 WhatsApp 等頂級訊息應用程式中不可或缺的功能。
將視圖錨定到鍵盤頂部有兩種常見的用例。第一種是建立鍵盤工具列,例如 Facebook 作曲器背景選擇器。

在這種情況下,鍵盤焦點集中在文字輸入欄位上,而輸入輔助視圖用於提供額外的鍵盤功能。此功能與輸入欄位的類型相關。在對應應用程式中,它可以是地址建議,而在文字編輯器中,它可以是 RTF 格式設定工具。
在這種情況下,擁有 <InputAccessoryView>
的 Objective-C UIResponder 應該很清楚。<TextInput>
已成為第一響應者,而在底層,這會變成 UITextView
或 UITextField
的實例。
第二種常見的情況是黏性文字輸入

在這裡,文字輸入實際上是輸入輔助視圖本身的一部分。這通常用於訊息應用程式中,使用者可以在捲動瀏覽先前的訊息串時撰寫訊息。
在此範例中,誰擁有 <InputAccessoryView>
?可以是 UITextView
或 UITextField
嗎?文字輸入位於輸入輔助視圖內部,這聽起來像是循環依賴。僅解決這個問題本身就是 另一篇部落格文章 的主題。劇透:擁有者是一個通用的 UIView
子類別,我們手動告知它 becomeFirstResponder。
API 設計
我們現在知道 <InputAccessoryView>
是什麼,以及我們想要如何使用它。下一步是設計一個對兩種用例都有意義的 API,並且可以與現有的 React Native 組件(例如 <TextInput>
)良好地協同運作。
對於鍵盤工具列,我們需要考慮以下幾點
- 我們希望能夠將任何通用的 React Native 視圖階層提升到
<InputAccessoryView>
中。 - 我們希望這個通用的且分離的視圖階層能夠接受觸控並能夠操作應用程式狀態。
- 我們希望將
<InputAccessoryView>
連結到特定的<TextInput>
。 - 我們希望能夠在多個文字輸入之間共用
<InputAccessoryView>
,而無需複製任何程式碼。
我們可以使用類似於 React portals 的概念來實現 #1。在此設計中,我們將 React Native 視圖傳送到由響應者基礎架構管理的 UIView
階層。由於 React Native 視圖呈現為 UIView,因此這實際上非常簡單 - 我們只需覆寫
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
並將所有子視圖管道傳輸到新的 UIView 階層。對於 #2,我們為 <InputAccessoryView>
設定了新的 RCTTouchHandler。狀態更新是透過使用常規事件回呼來實現的。對於 #3 和 #4,我們使用 nativeID 欄位,以便在建立 <TextInput>
組件期間在原生程式碼中找到輔助視圖 UIView 階層。此函數使用基礎原生文字輸入的 .inputAccessoryView
屬性。這樣做有效地將 <InputAccessoryView>
連結到其 ObjC 實作中的 <TextInput>
。
支援黏性文字輸入(情境 2)增加了更多限制。對於此設計,輸入輔助視圖具有作為子項的文字輸入,因此透過 nativeID 連結不是一種選擇。相反,我們將通用螢幕外 UIView
的 .inputAccessoryView
設定為我們的原生 <InputAccessoryView>
階層。透過手動告知此通用 UIView
成為第一響應者,階層會由響應者基礎架構掛載。前述部落格文章中徹底解釋了這個概念。
陷阱
當然,在建置此 API 時,並非一切都一帆風順。以下是我們遇到的一些陷阱,以及我們如何修正它們。
建置此 API 的最初想法涉及監聽 NSNotificationCenter
以取得 UIKeyboardWill(Show/Hide/ChangeFrame) 事件。此模式用於某些開源函式庫中,也用於 Facebook 應用程式的某些部分中。不幸的是,沒有及時呼叫 UIKeyboardDidChangeFrame
事件來更新滑動時的 <InputAccessoryView>
框架。此外,這些事件未捕捉到鍵盤高度的變化。這會產生一類錯誤,其表現如下

在 iPhone X 上,文字和表情符號鍵盤的高度不同。大多數使用鍵盤事件來操作文字輸入框架的應用程式都必須修正上述錯誤。我們的解決方案是承諾使用 .inputAccessoryView
屬性,這表示響應者基礎架構會處理此類框架更新。
我們遇到的另一個棘手的錯誤是避免 iPhone X 上的主畫面指示器。您可能會想,「Apple 開發了 safeAreaLayoutGuide 正是為了這個原因,這很簡單!」。我們和您一樣天真。第一個問題是,原生 <InputAccessoryView>
實作在即將出現之前沒有可錨定的視窗。沒關係,我們可以覆寫 -(BOOL)becomeFirstResponder
並在那裡強制執行版面配置約束。遵守這些約束會將輔助視圖向上推,但會出現另一個錯誤:
輸入輔助視圖成功避免了主畫面指示器,但現在可以看到不安全區域後面的內容。解決方案在於這個 radar。我將原生 <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>
);
}
}
儲存庫中提供了 黏性文字輸入的另一個範例。
我何時可以使用這個?
此功能實作的完整 commit 位於 此處。<InputAccessoryView>
將在即將發布的 v0.55.0 版本中提供。
鍵盤操作愉快 :)