返回

iOS React Native 添加键盘“完成”按钮

IOS

iOS React Native 中键盘添加“完成”按钮

为React Native iOS应用添加键盘“完成”按钮是一种增强用户体验的常见需求。当用户完成文本输入时,提供一个专门的按钮来关闭键盘,而非依赖点击屏幕空白区域等操作,可使应用更便捷易用。iOS允许开发者通过输入附件视图 (InputAccessoryView) 自定义键盘上方区域,此机制为实现目标提供了技术支持。

问题分析

默认情况下,iOS键盘不会自带一个“完成”按钮。通常,用户需要点击键盘以外的区域或者应用界面的其它组件来关闭键盘。这种交互可能不够直观或流畅。 添加一个明确的“完成”按钮可以让用户更容易控制键盘的显示和隐藏。 问题的核心是如何在React Native应用中,利用iOS的InputAccessoryView特性来实现这个功能。

解决方案一: 使用 InputAccessoryView 组件

React Native的TextInput组件提供了一个 inputAccessoryViewID属性,结合原生iOS模块可以创建一个自定义输入附件视图。这个方案的关键在于创建一个UIView实例,并将其设置到对应的UITextField上。

步骤:

  1. 创建自定义iOS原生模块: 创建一个Objective-C/Swift桥接文件,实现创建和配置 InputAccessoryView 的逻辑。 此模块接收inputAccessoryViewID和回调函数。
  2. 实现 JavaScript 桥接逻辑: 利用 NativeModules API将步骤1的 iOS 原生方法在 React Native 中调用。在TextInput 中设置 inputAccessoryViewID,使得输入视图生效。
  3. 处理点击事件: 在 Objective-C/Swift 文件中,当 "完成" 按钮被点击时,调用通过回调函数传入的事件处理,将事件回传给 React Native 层。 此时可以在React Native 层利用 TextInputblur()方法隐藏键盘。

示例代码: (原生 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;

解释:

  1. Objective-C/Swift桥接:
  • 创建了一个 RCTViewManager 的子类。它管理 InputAccessoryView 的创建和配置。 createAccessoryView方法会创建一个 UIToolbar, 添加一个 "完成" 按钮,并将其设置为 UITextFieldinputAccessoryView.
    - 使用responderChain获取当前激活的 UITextField, 并调用其 resignFirstResponder() 关闭键盘. 实现了"完成"按钮的事件处理。
  1. JavaScript 代码:
    • 利用 NativeModules 获取了 KeyboardAccessoryViewManager 的实例. 使用 inputAccessoryViewID 设置文本框使用对应的自定义输入视图。useEffect 仅调用一次设置逻辑. 当"完成"按钮点击时会执行传入 createAccessoryView 的回调方法 handleDonePress 关闭键盘。

注意: inputAccessoryViewID 需要与原生的 createAccessoryView 的参数一致, 使用唯一的 id.

注意事项

  • 在复杂的视图层次中, resignFirstResponder 的调用位置可能会影响性能。谨慎设计键盘关闭逻辑。
  • 此方案只针对iOS有效, 如果需要安卓, 需要实现不同的逻辑。
  • 对于具有大量输入字段的复杂界面,考虑管理多个输入附件视图实例。

此解决方案利用 iOS 平台的原生特性,为用户提供直观、便捷的交互方式,在React Native中实现了自定义键盘工具栏的效果,从而提升用户体验。