前几天在看响应者链条的时候,看到 UIResponder 有两个很有用的属性,但是自己不熟悉,就是 inputViewinputAccessoryView。原来自己练手的时候,需要这样的功能,但是自己不知道这两个属性,导致自己花费了很多时间。所以,就写下这篇,算是对不知道的知识的补充。

UITextField 的 inputView 和 inputAccessoryView

UITextField 的 inputView

默认情况下,当 UITextField 对象成为第一响应者的时候,系统会唤出系统键盘来接收用户的输入。

但是在有些时候,我们不希望唤出系统键盘,而是我们自定义的 view 来接收用户的输入,在这种情况下,inputView 就派上用场了。inputView 的作用就是让开发者提供自定义的 view 来获取用户的输入,当 UITextField 对象成为第一响应者的时候,系统会尝试唤出 inputView,如果 inputView 存在,就唤出开发者提供的 inputView;如果 inputView 不存在,也就是说这个属性的值是 nil(inputView 默认就是 nil),系统会唤出系统键盘。

现在假设当我们点击 UITextField 之后,显示出来的是一个 UIDatePicker。大概的实现代码:

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UITextField *textField;
@property (nonatomic, strong) UIDatePicker *datePicker;
@end

@implementation ViewController

- (UIDatePicker *)datePicker {
    if (!_datePicker) {
        _datePicker = [[UIDatePicker alloc] init];
        _datePicker.datePickerMode = UIDatePickerModeCountDownTimer;
    }
    return _datePicker;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.textField.inputView = self.datePicker;
}

@end

UITextField 的 inputAccessoryView

接下来说一下 inputAccessoryView 的作用,这个属性就是在 inputView 或者系统键盘上添加一个辅助的 view,例如下图高亮部分(图片截取自奇点):

inputAccessory Example

我们自己来添加一个 inputAccessoryView,大概的代码实现:

@interface ViewController () 
@property (nonatomic, weak) IBOutlet UITextField *textField;
@property (nonatomic, strong) UIToolbar *toolBar;
@end

@implementation ViewController

- (UIToolbar *)toolBar {
    if (!_toolBar) {
        _toolBar = [[UIToolbar alloc] init];
        _toolBar.bounds = (CGRect){CGPointZero, self.view.bounds.size.width, 49};
        UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                                                                                   target:nil
                                                                                   action:NULL];
        UIBarButtonItem *rightItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                                                                                   target:self
                                                                                   action:@selector(done)];
        _toolBar.items = @[spaceItem, rightItem];
    }
    return _toolBar;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.textField.inputAccessoryView = self.toolBar;
}

- (void)done {
    NSString *text = [NSString stringWithFormat:@"%.2lf s", self.datePicker.countDownDuration];
    if (self.textField.isFirstResponder) {
        self.textField.text = text;
        [self.textField resignFirstResponder];
    }
}

@end

UITextView 的自定义 inputViewinputAccessoryViewUITextField 类似。

UIReponder 的 inputView 和 inputAccessoryView

UIReponder 的 inputView

我们看一下 inputViewinputAccessoryViewUIResponder 的声明:

// Called and presented when object becomes first responder.  Goes up the responder chain.
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView NS_AVAILABLE_IOS(3_2);
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView NS_AVAILABLE_IOS(3_2);

这里我们看到 UIResponder 将这两个属性声明为 readonly,所以当我们使用例如:UIButton 的时候,我们就需要继承这些类,然后重新将这两个属性声明称 readwrite

假设我们点击一个 button 时,要显示一个 UIDatePicker,大概的代码实现:

@interface TSButton : UIButton
@property (nonatomic, strong, readwrite, nullable) UIView *inputView;
@property (nonatomic, strong, readwrite, nullable) UIView *inputAccessoryView;
@end

@implementation TSButton
// 1
- (BOOL)canBecomeFirstResponder {
    return YES;
}
// 2
- (BOOL)canResignFirstResponder {
    return YES;
}
@end

@interface ViewController ()
@property (nonatomic, weak) IBOutlet TSButton *showDatePickerButton;
@property (nonatomic, strong) UIDatePicker *datePicker;
@end

@implementation ViewController

- (UIDatePicker *)datePicker {
    if (!_datePicker) {
        _datePicker = [[UIDatePicker alloc] init];
        _datePicker.datePickerMode = UIDatePickerModeCountDownTimer;
    }
    return _datePicker;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.showDatePickerButton.inputView = self.datePicker;
}

- (IBAction)showDatePicker {
     // 3
    [self.showDatePickerButton becomeFirstResponder];
}

@end

我们在这里要注意有注释的地方,除却 UITextFieldUITextView 之外,因为这两个控件会自动成为第一响应者,其他的 UIResponder 子类想要成为第一响应者有两步要做:

  1. 覆写 canBecomeFirstResponder 并返回 YES;
  2. 主动调用 becomeFirstResponder 方法。

因为只有成为第一响应者,系统才会去唤出 inputViewinputAccessoryView,所以我们需要主动调用 becomeFirstResponder 方法。

reloadInputViews 用法

当对象是第一响应者时,调用这个方法来刷新 inputViewinputAccessoryView。这些 view 会立马被替换,没有动画。如果对象不是第一响应者,则这个方法就没有任何效果。

与 inputView 和 inputAccessoryView 相关的通知

我们有时会监听与系统键盘相关的通知,以便在系统键盘出现的时候,来调整我们的 UI 界面。同样,自定义的 inputView 出现或者消失时也会触发键盘相关的通知。我们同样可以监听 UIKeyboardWillShowNotification, UIKeyboardDidShowNotification, UIKeyboardWillHideNotification, 和 UIKeyboardDidHideNotification 通知来调整 UI 视图。这里需要注意的是,当有 inputAccessoryView 时,通知的 userInfo 中的高度数据,是 inputView 的高度加上 inputAccessoryView 的高度。

最后

UIResponder 还有 inputViewControllerinputAccessoryViewController 属性,这两个属性跟自定义键盘有关。下面是它们在 UIResponder 中的声明:

// For viewController equivalents of -inputView and -inputAccessoryView
// Called and presented when object becomes first responder.  Goes up the responder chain.
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController NS_AVAILABLE_IOS(8_0);
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController NS_AVAILABLE_IOS(8_0);

相关链接