iOS之自定義鍵盤(pán)

需求

在項(xiàng)目中有的時(shí)候需要對(duì)輸入框進(jìn)行重新定義咱旱,而且不能手動(dòng)的輸入一些內(nèi)容允坚,比如說(shuō)是類(lèi)似于下面的需求:

重定義鍵盤(pán)

這種的樣式的鍵盤(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了柿究。

點(diǎn)擊彈出自定義view

3.實(shí)現(xiàn)封裝

封裝自己的輸入框:
新建一個(gè)類(lèi)繼承自UITextField邮旷,比如wjCountryFlagTextField這個(gè)類(lèi),顯示的效果如下

國(guó)家選擇效果圖

上述的效果圖其實(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.provinceIndexself.cityIndex均為0,在點(diǎn)擊一次后瘩蚪,進(jìn)行賦值后泉懦,這兩個(gè)變量都有值了,再次進(jìn)入的時(shí)候就有可以顯式到之前選中的狀態(tài)疹瘦。也避免出現(xiàn)如下的bug:

再次選擇數(shù)據(jù)錯(cuò)誤的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)行完善。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末耿芹,一起剝皮案震驚了整個(gè)濱河市崭篡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吧秕,老刑警劉巖媚送,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寇甸,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)疗涉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)拿霉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人咱扣,你說(shuō)我怎么就攤上這事绽淘。” “怎么了闹伪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵沪铭,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我偏瓤,道長(zhǎng)杀怠,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任厅克,我火速辦了婚禮赔退,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己硕旗,他們只是感情好窗骑,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著漆枚,像睡著了一般创译。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上墙基,一...
    開(kāi)封第一講書(shū)人閱讀 52,785評(píng)論 1 314
  • 那天软族,我揣著相機(jī)與錄音,去河邊找鬼碘橘。 笑死互订,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的痘拆。 我是一名探鬼主播仰禽,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼纺蛆!你這毒婦竟也來(lái)了吐葵?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤桥氏,失蹤者是張志新(化名)和其女友劉穎温峭,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體字支,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凤藏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了堕伪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揖庄。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖欠雌,靈堂內(nèi)的尸體忽然破棺而出蹄梢,到底是詐尸還是另有隱情,我是刑警寧澤富俄,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布禁炒,位于F島的核電站,受9級(jí)特大地震影響霍比,放射性物質(zhì)發(fā)生泄漏幕袱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一悠瞬、第九天 我趴在偏房一處隱蔽的房頂上張望凹蜂。 院中可真熱鬧,春花似錦、人聲如沸玛痊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)擂煞。三九已至混弥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間对省,已是汗流浹背蝗拿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蒿涎,地道東北人哀托。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像劳秋,于是被迫代替她去往敵國(guó)和親仓手。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容