需求
在項(xiàng)目中有的時(shí)候需要對(duì)輸入框進(jìn)行重新定義咱旱,而且不能手動(dòng)的輸入一些內(nèi)容允坚,比如說(shuō)是類(lèi)似于下面的需求:
這種的樣式的鍵盤(pán)漫试,通過(guò)系統(tǒng)的輸入框是不能實(shí)現(xiàn)的泽台,所以我們需要自己定義下什荣。
實(shí)現(xiàn)思路
我們點(diǎn)擊普通的輸入框,彈出的一般就是鍵盤(pán)怀酷,我們可以從這個(gè)點(diǎn)擊輸入框的地方下手稻爬,看能否獲取到輸入框的點(diǎn)擊事件,如果能獲取點(diǎn)擊事件蜕依,我們就從這個(gè)地方截取到用戶(hù)的點(diǎn)擊事件桅锄,來(lái)自定義鍵盤(pán)。
1样眠、輸入行為的攔截
#pragma mark - UITextFieldDelegate
/**
是否允許開(kāi)始編輯
*/
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField;
/**
開(kāi)始編輯時(shí)調(diào)用,成為第一響應(yīng)者進(jìn)行調(diào)用
*/
- (void)textFieldDidBeginEditing:(UITextField *)textField;
/**
是否允許結(jié)束編輯
*/
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField;
/**
結(jié)束編輯的時(shí)候進(jìn)行調(diào)用
*/
- (void)textFieldDidEndEditing:(UITextField *)textField;
/**
是否允許改變文本框的內(nèi)容
*/
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string;
這里我們通過(guò)查看UITextField的代理可以得到上述的代理友瘤。
// 這個(gè)代理方法可以攔截到用戶(hù)的輸入
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
return NO;
}
當(dāng)輸入框輸入內(nèi)容的時(shí)候,我們需要讓輸入框不能進(jìn)行響應(yīng)檐束,也就相當(dāng)于不能進(jìn)行輸入辫秧,從而實(shí)現(xiàn)了攔截用戶(hù)的輸入行為。
2被丧、點(diǎn)擊輸入框彈出自定義的view
既然上面的代理方法是可以完成輸入的攔截行為盟戏,那么就需要自定義輸入框的點(diǎn)擊彈出的view绪妹。
textField有個(gè)屬性:
@property (nullable, readwrite, strong) UIView *inputView;
textField.inputView = [[UISwitch alloc] init];
在點(diǎn)擊輸入框的時(shí)候就不會(huì)彈出鍵盤(pán),而是彈出的自定義的view了柿究。
3.實(shí)現(xiàn)封裝
封裝自己的輸入框:
新建一個(gè)類(lèi)繼承自UITextField邮旷,比如wjCountryFlagTextField
這個(gè)類(lèi),顯示的效果如下
上述的效果圖其實(shí)就是把上面的swich開(kāi)關(guān)修改成了一個(gè)UIPickView蝇摸,實(shí)現(xiàn)的原理大同小異婶肩。
下面就來(lái)實(shí)現(xiàn)下:
3.1.數(shù)據(jù)源
國(guó)旗和國(guó)家這寫(xiě)名字源來(lái)自plist文件,本地的比較友好貌夕。律歼。。啡专。
從plist加載數(shù)據(jù)苗膝,先創(chuàng)建一個(gè)模型,然后在模型中寫(xiě)個(gè)轉(zhuǎn)模型的方法
3.1.1數(shù)據(jù)源數(shù)組
// 數(shù)據(jù)源數(shù)組
- (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模型
根據(jù)plist文件創(chuàng)建屬性植旧,不多說(shuō)。添加一個(gè)字典轉(zhuǎn)模型的方法:
+ (instancetype)modelWithDict:(NSDictionary *)dict {
wjCountryFlagModel *model = [[self alloc] init];
[model setValuesForKeysWithDictionary:dict]; // 如果用KVC方法進(jìn)行賦值的話(huà)离唐,必須要求 model和plist的字段名是一致的
return model;
}
3.2.控件
既然是封裝就要求不管是從storyboard還是代碼創(chuàng)建都要能夠調(diào)用病附,所以我們實(shí)現(xiàn)下面這兩個(gè)方法。
3.2.1初始化
// 從xib加載的
- (void)awakeFromNib {
[super awakeFromNib];
// 初始化文本框
[self setUpTextField];
}
// 代碼加載的
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setUpTextField];
}
return self;
}
實(shí)現(xiàn)輸入框的初始化
- (void)setUpTextField {
// 創(chuàng)建pickView
UIPickerView *pickView = [[UIPickerView alloc] init];
pickView.delegate = self;
pickView.dataSource = self;
// 修改文本框彈出鍵盤(pán)的類(lèi)型
self.inputView = pickView;
}
3.2.2實(shí)現(xiàn)pickView的數(shù)據(jù)源協(xié)議和代理
通過(guò)效果圖得知這個(gè)pickView只有1列亥鬓,那么需要實(shí)現(xiàn)pickView的dataSource和delegate完沪。
#pragma mark - UIPickerViewDataSource
// 列數(shù)
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
// 行數(shù)
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return self.dataArray.count;
}
#pragma mark - UIPickerViewDelegate
// 設(shè)置行高
- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component {
return 80;
}
在pickView的代理中有幾個(gè)代理需要說(shuō)下,這個(gè)幾個(gè)代理是在pickView顯示文字或者控件的嵌戈。
// 這是返回的字符串類(lèi)型的
- (nullable NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component __TVOS_PROHIBITED;
// 返回的是富文本類(lèi)型
- (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
// 顯示一個(gè)view在pickView上
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(nullable UIView *)view __TVOS_PROHIBITED;
3.3.自定義控件
很顯然通過(guò)效果圖覆积,我們需要在pickView顯示的是文字和圖片,所以我們需要自定義顯示的控件熟呛,繼承自UIView宽档,來(lái)展示數(shù)據(jù)源。
添加一個(gè)類(lèi)方法庵朝,方便創(chuàng)建吗冤。
3.3.1創(chuàng)建view
// 我這里是通過(guò)xib創(chuàng)建的。
+ (instancetype)countryFlagView {
return [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([wjCountryFlagView class]) owner:nil options:nil].lastObject;
}
3.3.2展示數(shù)據(jù)
其實(shí)這個(gè)和自定義的tableView的做法類(lèi)似九府,通過(guò)重寫(xiě)model的set方法椎瘟,進(jìn)行展示數(shù)據(jù)源。
- (void)setModel:(wjCountryFlagModel *)model {
_model = model;
self.wjCountryNameLabel.text = model.name;
self.wjFlagImageView.image = [UIImage imageNamed:model.icon];
}
以上完成侄旬,需要回到控件中去展示數(shù)據(jù)
- (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.填充文字
以上基本完成了功能肺蔚,下面完成選擇完成后,文字的填充儡羔。
// 把當(dāng)前選中的展示到文本框中
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
wjCountryFlagModel *model = self.dataArray[row];
self.text = model.name;
}
到此選擇國(guó)家的輸入框的功能基本完成宣羊,選擇省市的輸入框功能的做法和選擇國(guó)家的方法類(lèi)似璧诵,只是不用進(jìn)行自定義控件,直接展示字符類(lèi)型的數(shù)據(jù)就可以段只。
4.對(duì)于生日的控件的說(shuō)明
在選擇生日的控件中腮猖,我們可以用UIDatePicker
作為自定義的控件來(lái)攔截掉鍵盤(pán)。
但是對(duì)于這個(gè)UIDatePicker
控件來(lái)說(shuō)赞枕,不像UIPickView
一樣有類(lèi)似于監(jiān)聽(tīng)數(shù)據(jù)改變的代理方法澈缺,所以需要另想辦法實(shí)現(xiàn)。
UIDatePicker
這個(gè)控件是繼承自UIControl
炕婶,我們就可以考慮使用- addTarget: action: forControlEvents
方法來(lái)監(jiān)聽(tīng)日期的改變姐赡。
// 日期發(fā)生改變就要調(diào)用
- (void)dateChange:(UIDatePicker *)datePick {
NSLog(@"123");
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyy-MM-dd";
// 把當(dāng)前日期轉(zhuǎn)成字符串
self.text = [dateFormatter stringFromDate:datePick.date];
}
5.關(guān)于省市控件的一些說(shuō)明
這個(gè)控件顯示的是各個(gè)省的名字和各省所轄的市州的名字,所以在改變省province那一列的時(shí)候柠掂,市city那一列也應(yīng)該跟著變化项滑,所以就需要記錄下當(dāng)前省province所在行數(shù),然后再去刷新pickview涯贞。
// 在代理中先記錄下來(lái)province所選擇的行號(hào)
- (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的行號(hào)枪狂,就可以得知第二列市city的數(shù)據(jù)了。
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
if (component == 0) {
return self.dataArray.count;
} else {
// 第二列應(yīng)該展示的總行數(shù)
wjProvinceModel *model = self.dataArray[self.provinceIndex];
NSArray *cityArray = model.cities;
return cityArray.count;
}
}
展示數(shù)據(jù)
- (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:
這個(gè)代理方法宋渔,把所選擇省市的數(shù)據(jù)展示到輸入框中州疾。這個(gè)方法也就是之前確定省province所在行數(shù)所調(diào)用的代理方法。
6.細(xì)節(jié)補(bǔ)充
到此皇拣,需求已經(jīng)基本完成了严蓖,但是開(kāi)始點(diǎn)擊的輸入框的時(shí)候,我們希望選擇第一個(gè)數(shù)據(jù)源氧急,我們就需要進(jìn)行初始化操作颗胡。
下面就需要在自定義的textField中添加初始化操作的方法,且暴露出來(lái)吩坝。
// 初始化方法
- (void)initWithText {
[self pickerView:self.pickView didSelectRow:self.provinceIndex inComponent:0];
[self pickerView:self.pickView didSelectRow:self.cityIndex inComponent:1];}
初始化方法其實(shí)就是重新調(diào)用了代理方法毒姨,然后使得選擇的列為之前選中的列,選擇的行為之前選中的行钉寝。這樣在第一次進(jìn)入的時(shí)候手素,self.provinceIndex
和self.cityIndex
均為0,在點(diǎn)擊一次后瘩蚪,進(jìn)行賦值后泉懦,這兩個(gè)變量都有值了,再次進(jìn)入的時(shí)候就有可以顯式到之前選中的狀態(tài)疹瘦。也避免出現(xiàn)如下的bug:
輸入框有個(gè)代理方法崩哩,在輸入框開(kāi)始編輯的時(shí)候就開(kāi)始調(diào)用。
/**
開(kāi)始編輯時(shí)調(diào)用,成為第一響應(yīng)者進(jìn)行調(diào)用
* 這是對(duì)每個(gè)類(lèi)都創(chuàng)建了一個(gè)初始化方法,針對(duì)每個(gè)輸入框進(jìn)行調(diào)用不同的初始化方法
*/
- (void)textFieldDidBeginEditing:(UITextField *)textField {
// 使用分類(lèi)邓嘹,對(duì)方法進(jìn)行重寫(xiě)
// 讓當(dāng)前的文本框選中第一個(gè)
if (textField == self.wjCountryTextField) {
[textField initWithText];
} else if (textField == self.wjBirthdayTextField) {
[textField initWithBirthday];
} else {
[textField initWithProvinceAndCity];
}
}
以上的代碼中酣栈,textfield能直接調(diào)用每個(gè)初始化方法的原因是我對(duì)UITextField寫(xiě)一個(gè)分類(lèi),添加了每個(gè)輸入框的初始化方法汹押。
簡(jiǎn)化下:
// 每個(gè)輸入框都實(shí)現(xiàn)同一個(gè)方法名的方法矿筝。
- (void)textFieldDidBeginEditing:(UITextField *)textField {
// 使用分類(lèi),對(duì)方法進(jìn)行重寫(xiě)
[textField initWithText];
}
最后獻(xiàn)上demo的地址棚贾,如果你覺(jué)得還可以的話(huà)窖维,GitHub上給個(gè)贊唄!
以上完成功能妙痹,如果下次需要相似的功能铸史,只需把這個(gè)拖入到相關(guān)的工程中,就可以使用怯伊。如果如覺(jué)得代碼創(chuàng)建類(lèi)的方法不夠用的話(huà)琳轿,還可以自行的添加方法進(jìn)行完善。