需求
在项目中有的时候需要对输入框进行重新定义,而且不能手动的输入一些内容,比如说是类似于下面的需求:
这种的样式的键盘,通过系统的输入框是不能实现的,所以我们需要自己定义下。
实现思路
我们点击普通的输入框,弹出的一般就是键盘,我们可以从这个点击输入框的地方下手,看能否获取到输入框的点击事件,如果能获取点击事件,我们就从这个地方截取到用户的点击事件,来自定义键盘。
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.provinceIndex
和self.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上给个赞呗! 以上完成功能,如果下次需要相似的功能,只需把这个拖入到相关的工程中,就可以使用。如果如觉得代码创建类的方法不够用的话,还可以自行的添加方法进行完善。