iOS React Native 添加键盘“完成”按钮
2025-02-02 15:16:39
iOS React Native 中键盘添加“完成”按钮
为React Native iOS应用添加键盘“完成”按钮是一种增强用户体验的常见需求。当用户完成文本输入时,提供一个专门的按钮来关闭键盘,而非依赖点击屏幕空白区域等操作,可使应用更便捷易用。iOS允许开发者通过输入附件视图 (InputAccessoryView) 自定义键盘上方区域,此机制为实现目标提供了技术支持。
问题分析
默认情况下,iOS键盘不会自带一个“完成”按钮。通常,用户需要点击键盘以外的区域或者应用界面的其它组件来关闭键盘。这种交互可能不够直观或流畅。 添加一个明确的“完成”按钮可以让用户更容易控制键盘的显示和隐藏。 问题的核心是如何在React Native应用中,利用iOS的InputAccessoryView特性来实现这个功能。
解决方案一: 使用 InputAccessoryView
组件
React Native的TextInput
组件提供了一个 inputAccessoryViewID
属性,结合原生iOS模块可以创建一个自定义输入附件视图。这个方案的关键在于创建一个UIView
实例,并将其设置到对应的UITextField
上。
步骤:
- 创建自定义iOS原生模块: 创建一个Objective-C/Swift桥接文件,实现创建和配置
InputAccessoryView
的逻辑。 此模块接收inputAccessoryViewID
和回调函数。 - 实现 JavaScript 桥接逻辑: 利用
NativeModules
API将步骤1的 iOS 原生方法在 React Native 中调用。在TextInput
中设置inputAccessoryViewID
,使得输入视图生效。 - 处理点击事件: 在 Objective-C/Swift 文件中,当 "完成" 按钮被点击时,调用通过回调函数传入的事件处理,将事件回传给 React Native 层。 此时可以在React Native 层利用
TextInput
的blur()
方法隐藏键盘。
示例代码: (原生 Objective-C 代码 - YourProjectName.m )
#import <React/RCTBridgeModule.h>
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(KeyboardAccessoryViewManager, RCTViewManager)
RCT_EXTERN_METHOD(createAccessoryView:(nonnull NSString *)viewID withCallback:(RCTResponseSenderBlock)callback)
@end
#import "YourProjectName.h"
#import <UIKit/UIKit.h>
#import <React/RCTConvert.h>
@implementation KeyboardAccessoryViewManager
RCT_EXPORT_MODULE()
- (UIView *)view {
return [[UIView alloc] init];
}
RCT_EXPORT_METHOD(createAccessoryView:(NSString *)viewID withCallback:(RCTResponseSenderBlock)callback) {
dispatch_async(dispatch_get_main_queue(), ^{
UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 44)];
toolbar.barStyle = UIBarStyleDefault;
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:@"完成" style:UIBarButtonItemStyleDone target:self action:@selector(doneButtonTapped:)];
[toolbar setItems:@[flexibleSpace, doneButton] animated:YES];
if ( [self getFocusedTextInput] ) {
[[self getFocusedTextInput] setInputAccessoryView:toolbar];
} else {
NSLog(@" No textinput in focus.");
}
self.callbackDictionary[viewID] = callback;
});
}
- (void) doneButtonTapped: (id) sender {
//Get the associated id with keyboard and call its callback function
UITextField* focusedTextField = [self getFocusedTextInput];
if(focusedTextField)
{
UIView* inputAccessoryView = focusedTextField.inputAccessoryView;
NSString* inputViewId = [self getAccessoryIdFromView:inputAccessoryView];
if( inputViewId && self.callbackDictionary[inputViewId])
self.callbackDictionary[inputViewId](@[@"doneButtonTapped"]);
[focusedTextField resignFirstResponder];
}
}
-(UITextField*) getFocusedTextInput {
UIView *focusedView = [[[UIApplication sharedApplication] keyWindow] performSelector: NSSelectorFromString(@"firstResponder")];
if( [focusedView isKindOfClass:[UITextField class]] ){
return (UITextField *)focusedView;
}else{
return nil;
}
}
-(NSString*) getAccessoryIdFromView: (UIView*)view {
if ([view respondsToSelector:@selector(reactTag)] ){
NSNumber* reactTagNumber = (NSNumber*)[view performSelector:@selector(reactTag)];
return [NSString stringWithFormat:@"%lld", [reactTagNumber longLongValue]];
} else {
return nil;
}
}
- (NSMutableDictionary *)callbackDictionary {
if(!_callbackDictionary) _callbackDictionary = [NSMutableDictionary new];
return _callbackDictionary;
}
@end
示例代码 (原生 Swift 代码- YourProjectName.swift)
import Foundation
import UIKit
@objc(KeyboardAccessoryViewManager)
class KeyboardAccessoryViewManager: RCTViewManager {
override func view() -> UIView! {
return UIView()
}
private var callbackDictionary: [String: RCTResponseSenderBlock] = [:]
@objc func createAccessoryView(_ viewID: String, withCallback callback: @escaping RCTResponseSenderBlock) {
DispatchQueue.main.async {
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44))
toolbar.barStyle = .default
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(title: "完成", style: .done, target: self, action: #selector(self.doneButtonTapped(sender:)))
toolbar.setItems([flexibleSpace, doneButton], animated: true)
if let focusedTextInput = self.getFocusedTextInput() {
focusedTextInput.inputAccessoryView = toolbar
}else {
NSLog("No textinput in focus.")
}
self.callbackDictionary[viewID] = callback
}
}
@objc private func doneButtonTapped(sender: Any) {
guard let focusedTextField = getFocusedTextInput() else { return }
guard let inputAccessoryView = focusedTextField.inputAccessoryView else { return }
guard let inputViewId = getAccessoryIdFromView(view: inputAccessoryView) else { return }
guard let callback = callbackDictionary[inputViewId] else { return }
callback(["doneButtonTapped"])
focusedTextField.resignFirstResponder()
}
private func getFocusedTextInput() -> UITextField? {
let focusedView = UIApplication.shared.keyWindow?.perform(Selector(("_firstResponder"))).takeUnretainedValue()
if focusedView is UITextField {
return focusedView as? UITextField
} else {
return nil
}
}
private func getAccessoryIdFromView(view: UIView) -> String? {
if let tagNumber = view.perform(Selector(("reactTag"))).takeUnretainedValue() as? NSNumber {
return String(describing: tagNumber.int64Value)
}
else {
return nil
}
}
}
示例代码(React Native Javascript代码):
import React, { useState, useRef } from 'react';
import { TextInput, View, NativeModules } from 'react-native';
const { KeyboardAccessoryViewManager } = NativeModules;
const MyComponent = () => {
const [text, setText] = useState('');
const textInputRef = useRef(null);
const accessoryViewId = 'uniqueId123';
const handleDonePress = () => {
//Close the Keyboard if you like.
textInputRef.current?.blur();
}
//call native method once to configure keyboard input accesssoryView on mount
React.useEffect( ()=> {
KeyboardAccessoryViewManager.createAccessoryView(accessoryViewId,
(event)=>{ handleDonePress(); }
);
}, []);
return (
<View>
<TextInput
ref={textInputRef}
value={text}
onChangeText={setText}
placeholder="输入文本"
inputAccessoryViewID={accessoryViewId} // <--- key here
/>
</View>
);
};
export default MyComponent;
解释:
- Objective-C/Swift桥接:
- 创建了一个
RCTViewManager
的子类。它管理InputAccessoryView
的创建和配置。createAccessoryView
方法会创建一个UIToolbar
, 添加一个 "完成" 按钮,并将其设置为UITextField
的inputAccessoryView
.
- 使用responderChain
获取当前激活的UITextField
, 并调用其resignFirstResponder()
关闭键盘. 实现了"完成"按钮的事件处理。
- JavaScript 代码:
- 利用
NativeModules
获取了KeyboardAccessoryViewManager
的实例. 使用inputAccessoryViewID
设置文本框使用对应的自定义输入视图。useEffect
仅调用一次设置逻辑. 当"完成"按钮点击时会执行传入createAccessoryView
的回调方法handleDonePress
关闭键盘。
- 利用
注意: inputAccessoryViewID
需要与原生的 createAccessoryView
的参数一致, 使用唯一的 id.
注意事项
- 在复杂的视图层次中,
resignFirstResponder
的调用位置可能会影响性能。谨慎设计键盘关闭逻辑。 - 此方案只针对iOS有效, 如果需要安卓, 需要实现不同的逻辑。
- 对于具有大量输入字段的复杂界面,考虑管理多个输入附件视图实例。
此解决方案利用 iOS 平台的原生特性,为用户提供直观、便捷的交互方式,在React Native中实现了自定义键盘工具栏的效果,从而提升用户体验。