为 React Native 构建 <InputAccessoryView>
动机
三年前,GitHub 上有一个 issue 被提出,要求 React Native 支持输入辅助视图。

在接下来的几年里,这个问题上出现了无数的“+1”,各种变通方法,但 React Native 却没有具体的改变——直到今天。从 iOS 开始,我们正在公开一个 API 用于访问原生输入附件视图,我们很高兴分享我们是如何构建它的。
背景
输入附件视图到底是什么?阅读苹果的开发者文档,我们了解到它是一个自定义视图,当接收器成为第一个响应者时,它可以锚定在系统键盘的顶部。任何继承自UIResponder
的都可以将.inputAccessoryView
属性重新声明为读写,并在此处管理一个自定义视图。响应者基础结构挂载视图,并使其与系统键盘保持同步。解散键盘的手势,如拖动或点击,在框架级别应用于输入附件视图。这使得我们可以构建具有交互式键盘解散功能的内容,这是像 iMessage 和 WhatsApp 这样的顶级消息应用程序中不可或缺的功能。
将视图锚定到键盘顶部有两种常见用例。第一种是创建键盘工具栏,例如Facebook撰写器背景选择器。

在这种情况下,键盘聚焦在文本输入字段上,输入附件视图用于提供额外的键盘功能。此功能与输入字段的类型相关。在地图应用程序中,它可能是地址建议,或者在文本编辑器中,它可能是富文本格式工具。
在这种情况下,拥有<InputAccessoryView>
的 Objective-C UIResponder 应该很清楚。<TextInput>
已经成为第一个响应者,在底层这变成了一个UITextView
或UITextField
实例。
第二种常见情况是固定文本输入

这里,文本输入实际上是输入辅助视图本身的一部分。这通常用于消息应用程序中,用户可以在滚动浏览之前的消息线程时撰写消息。
在这个例子中,谁拥有<InputAccessoryView>
?它可以再次是UITextView
或UITextField
吗?文本输入在输入附件视图内部,这听起来像是一个循环依赖。单独解决这个问题本身就是另一篇博客文章。剧透:所有者是一个通用的UIView
子类,我们手动告诉它becomeFirstResponder。
API设计
我们现在知道`<InputAccessoryView>`是什么,以及我们想如何使用它。下一步是设计一个对两种用例都适用,并且与现有React Native组件(如`<TextInput>`)良好协作的API。
对于键盘工具栏,有几件事需要考虑:
- 我们希望能够将任何通用 React Native 视图层次结构提升到
<InputAccessoryView>
中。 - 我们希望这个通用且分离的视图层次结构能够接受触摸并能够操纵应用程序状态。
- 我们希望将
<InputAccessoryView>
链接到特定的<TextInput>
。 - 我们希望能够在多个文本输入之间共享一个
<InputAccessoryView>
,而无需复制任何代码。
我们可以使用类似于React portals的概念来实现 #1。在这个设计中,我们将 React Native 视图传送到由响应者基础结构管理的UIView
层次结构。由于 React Native 视图渲染为 UIViews,这实际上非常简单——我们只需覆盖
- (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
属性,这意味着响应者基础设施会处理此类帧更新。
我们遇到的另一个棘手的 bug 是避免 iPhone X 上的 Home Bar。你可能会想,“苹果开发safeAreaLayoutGuide就是为了这个,这很简单!”我们同样天真。第一个问题是,原生的<InputAccessoryView>
实现在它即将出现之前没有窗口可以锚定。没关系,我们可以重写-(BOOL)becomeFirstResponder
并在那里强制执行布局约束。遵守这些约束会将附件视图向上推,但又出现了另一个 bug:
输入附件视图成功地避开了 Home Bar,但现在不安全区域后面的内容可见。解决方案在于这个雷达。我将原生的<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 版本中提供。
快乐打字 :)