iOS之自定义键盘

需求

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

重定义键盘

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

实现思路

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

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了。

点击弹出自定义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:

再次选择数据错误的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上给个赞呗!
以上完成功能,如果下次需要相似的功能,只需把这个拖入到相关的工程中,就可以使用。如果如觉得代码创建类的方法不够用的话,还可以自行的添加方法进行完善。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容