需求

在项目中有的时候需要对输入框进行重新定义,而且不能手动的输入一些内容,比如说是类似于下面的需求:



这种的样式的键盘,通过系统的输入框是不能实现的,所以我们需要自己定义下。

实现思路

我们点击普通的输入框,弹出的一般就是键盘,我们可以从这个点击输入框的地方下手,看能否获取到输入框的点击事件,如果能获取点击事件,我们就从这个地方截取到用户的点击事件,来自定义键盘。

1、输入行为的拦截

#pragma mark - UITextFieldDelegate
/**
 是否允许开始编辑
 */
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField;

/**
 开始编辑时调用,成为第一响应者进行调用
 */
- (void)textFieldDidBeginEditing:(UITextField *)textField;

/**
 是否允许结束编辑
 */
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField;

/**
 结束编辑的时候进行调用
 */
- (void)textFieldDidEndEditing:(UITextField *)textField;

/**
 是否允许改变文本框的内容
 */
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string;
复制代码

这里我们通过查看UITextField的代理可以得到上述的代理。

// 这个代理方法可以拦截到用户的输入
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    return NO;
}
复制代码

当输入框输入内容的时候,我们需要让输入框不能进行响应,也就相当于不能进行输入,从而实现了拦截用户的输入行为。

2、点击输入框弹出自定义的view

既然上面的代理方法是可以完成输入的拦截行为,那么就需要自定义输入框的点击弹出的view。 textField有个属性:

@property (nullable, readwrite, strong) UIView *inputView;             
复制代码
textField.inputView = [[UISwitch alloc] init];
复制代码

在点击输入框的时候就不会弹出键盘,而是弹出的自定义的view了。



3.实现封装

封装自己的输入框: 新建一个类继承自UITextField,比如wjCountryFlagTextField这个类,显示的效果如下


上述的效果图其实就是把上面的swich开关修改成了一个UIPickView,实现的原理大同小异。 下面就来实现下:

3.1.数据源

国旗和国家这写名字源来自plist文件,本地的比较友好。。。。 从plist加载数据,先创建一个模型,然后在模型中写个转模型的方法

3.1.1数据源数组
// 数据源数组
- (NSArray *)dataArray {
    if (!_dataArray) {
        NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil]];
        NSMutableArray *modelArray = [NSMutableArray array];
        for (NSDictionary *dict in array) {
            wjCountryFlagModel *model = [wjCountryFlagModel modelWithDict:dict];
            [modelArray addObject:model];
        }
        _dataArray = [modelArray copy];
    }
    return _dataArray;
}
复制代码

3.1.2模型

根据plist文件创建属性,不多说。添加一个字典转模型的方法:

+ (instancetype)modelWithDict:(NSDictionary *)dict {
    wjCountryFlagModel *model = [[self alloc] init];
    [model setValuesForKeysWithDictionary:dict]; // 如果用KVC方法进行赋值的话,必须要求 model和plist的字段名是一致的
    return model;
}
复制代码

3.2.控件

既然是封装就要求不管是从storyboard还是代码创建都要能够调用,所以我们实现下面这两个方法。

3.2.1初始化
// 从xib加载的
- (void)awakeFromNib {
    [super awakeFromNib];
    // 初始化文本框
    [self setUpTextField];
}

// 代码加载的
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self setUpTextField];
    }
    return self;
}
复制代码

实现输入框的初始化

- (void)setUpTextField {
    // 创建pickView
    UIPickerView *pickView = [[UIPickerView alloc] init];
    pickView.delegate = self;
    pickView.dataSource = self;
    
    // 修改文本框弹出键盘的类型
    self.inputView = pickView;  
}
复制代码
3.2.2实现pickView的数据源协议和代理

通过效果图得知这个pickView只有1列,那么需要实现pickView的dataSource和delegate。

#pragma mark - UIPickerViewDataSource
// 列数
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
    return 1;
}

// 行数
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
    return self.dataArray.count; 
}
复制代码
#pragma mark - UIPickerViewDelegate
// 设置行高
- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component {
    return 80;
}
复制代码

在pickView的代理中有几个代理需要说下,这个几个代理是在pickView显示文字或者控件的。

// 这是返回的字符串类型的
- (nullable NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component __TVOS_PROHIBITED;
// 返回的是富文本类型
- (nullable NSAttributedString *)pickerView:(UIPickerView *)pickerView attributedTitleForRow:(NSInteger)row forComponent:(NSInteger)component NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED; // attributed title is favored if both methods are implemented
// 显示一个view在pickView上
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(nullable UIView *)view __TVOS_PROHIBITED;

复制代码

3.3.自定义控件

很显然通过效果图,我们需要在pickView显示的是文字和图片,所以我们需要自定义显示的控件,继承自UIView,来展示数据源。 添加一个类方法,方便创建。

3.3.1创建view
// 我这里是通过xib创建的。
+ (instancetype)countryFlagView {
    return [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([wjCountryFlagView class]) owner:nil options:nil].lastObject;
}
复制代码
3.3.2展示数据

其实这个和自定义的tableView的做法类似,通过重写model的set方法,进行展示数据源。

- (void)setModel:(wjCountryFlagModel *)model {
    _model = model;
    self.wjCountryNameLabel.text = model.name;
    self.wjFlagImageView.image = [UIImage imageNamed:model.icon];
}
复制代码

以上完成,需要回到控件中去展示数据

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
    wjCountryFlagView *countryFlagView = [wjCountryFlagView countryFlagView];
    countryFlagView.model = self.dataArray[row];
    return countryFlagView;
}
复制代码

3.4.填充文字

以上基本完成了功能,下面完成选择完成后,文字的填充。

// 把当前选中的展示到文本框中
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
    wjCountryFlagModel *model = self.dataArray[row];
    self.text = model.name;
}
复制代码

到此选择国家的输入框的功能基本完成,选择省市的输入框功能的做法和选择国家的方法类似,只是不用进行自定义控件,直接展示字符类型的数据就可以。

4.对于生日的控件的说明

在选择生日的控件中,我们可以用UIDatePicker作为自定义的控件来拦截掉键盘。 但是对于这个UIDatePicker控件来说,不像UIPickView一样有类似于监听数据改变的代理方法,所以需要另想办法实现。 UIDatePicker这个控件是继承自UIControl,我们就可以考虑使用- addTarget: action: forControlEvents方法来监听日期的改变。

// 日期发生改变就要调用
- (void)dateChange:(UIDatePicker *)datePick {
    NSLog(@"123");
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy-MM-dd";
    // 把当前日期转成字符串
    self.text = [dateFormatter stringFromDate:datePick.date];
}
复制代码

5.关于省市控件的一些说明

这个控件显示的是各个省的名字和各省所辖的市州的名字,所以在改变省province那一列的时候,市city那一列也应该跟着变化,所以就需要记录下当前省province所在行数,然后再去刷新pickview。

// 在代理中先记录下来province所选择的行号
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
    if (component == 0) {
        // 所选择省的index
        self.provinceIndex = row;
        [pickerView selectRow:0 inComponent:1 animated:YES];
        [pickerView reloadAllComponents];
    }
}
复制代码

得到第一列省province的行号,就可以得知第二列市city的数据了。

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
    if (component == 0) {
        return self.dataArray.count;
    } else {
        // 第二列应该展示的总行数
        wjProvinceModel *model = self.dataArray[self.provinceIndex];
        NSArray *cityArray = model.cities;
        return cityArray.count;
    }
}
复制代码

展示数据

- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
    if (component == 0) {
        wjProvinceModel *model = self.dataArray[row];
        return model.name;
    } else {
        wjProvinceModel *model = self.dataArray[self.provinceIndex];
        return model.cities[row];
    }
}
复制代码

最后在- (void)pickerView: didSelectRow: inComponent:这个代理方法,把所选择省市的数据展示到输入框中。这个方法也就是之前确定省province所在行数所调用的代理方法。

6.细节补充

到此,需求已经基本完成了,但是开始点击的输入框的时候,我们希望选择第一个数据源,我们就需要进行初始化操作。 下面就需要在自定义的textField中添加初始化操作的方法,且暴露出来。

// 初始化方法 
- (void)initWithText {
    [self pickerView:self.pickView didSelectRow:self.provinceIndex inComponent:0];
    [self pickerView:self.pickView didSelectRow:self.cityIndex inComponent:1];}
复制代码

初始化方法其实就是重新调用了代理方法,然后使得选择的列为之前选中的列,选择的行为之前选中的行。这样在第一次进入的时候,self.provinceIndexself.cityIndex均为0,在点击一次后,进行赋值后,这两个变量都有值了,再次进入的时候就有可以显式到之前选中的状态。也避免出现如下的bug:


输入框有个代理方法,在输入框开始编辑的时候就开始调用。

/**
 开始编辑时调用,成为第一响应者进行调用
 * 这是对每个类都创建了一个初始化方法,针对每个输入框进行调用不同的初始化方法
 */
- (void)textFieldDidBeginEditing:(UITextField *)textField {
    // 使用分类,对方法进行重写
    // 让当前的文本框选中第一个
    if (textField == self.wjCountryTextField) {
        [textField initWithText];
    } else if (textField == self.wjBirthdayTextField) {
        [textField initWithBirthday];
    } else {
        [textField initWithProvinceAndCity];
    }
}
复制代码

以上的代码中,textfield能直接调用每个初始化方法的原因是我对UITextField写一个分类,添加了每个输入框的初始化方法。 简化下:

// 每个输入框都实现同一个方法名的方法。
- (void)textFieldDidBeginEditing:(UITextField *)textField {
    // 使用分类,对方法进行重写
    [textField initWithText];
}
复制代码

最后献上demo的地址,如果你觉得还可以的话,GitHub上给个赞呗! 以上完成功能,如果下次需要相似的功能,只需把这个拖入到相关的工程中,就可以使用。如果如觉得代码创建类的方法不够用的话,还可以自行的添加方法进行完善。