iOS開(kāi)發(fā)-UITextField的那點(diǎn)事

前言

UITextField被用作項(xiàng)目中獲取用戶信息的重要控件爪瓜,但是在實(shí)際應(yīng)用中存在的不少的坑:修改keyboardType來(lái)限制鍵盤(pán)的類型谊路,卻難以限制第三方鍵盤(pán)的輸入類型赊锚;在代理中限制了輸入長(zhǎng)度以及輸入的文本類型成箫,但是卻抵不住中文輸入的聯(lián)想;鍵盤(pán)彈起時(shí)遮住輸入框腹殿,需要接收鍵盤(pán)彈起收回的通知独悴,然后計(jì)算坐標(biāo)實(shí)現(xiàn)移動(dòng)動(dòng)畫(huà)例书。

對(duì)于上面這些問(wèn)題,蘋(píng)果提供給我們文本輸入框的同時(shí)并不提供解決方案刻炒,因此本文將使用category+runtime的方式解決上面提到的這些問(wèn)題决采,本文假設(shè)讀者已經(jīng)清楚從UITextField成為第一響應(yīng)者到結(jié)束編輯過(guò)程中的事件調(diào)用流程。

輸入限制

最常見(jiàn)的輸入限制是手機(jī)號(hào)碼以及金額坟奥,前者文本中只能存在純數(shù)字树瞭,后者文本中還能包括小數(shù)。筆者暫時(shí)定義了三種枚舉狀態(tài)用來(lái)表示三種文本限制:

typedef NS_ENUM(NSInteger, LXDRestrictType)
{
    LXDRestrictTypeOnlyNumber = 1,      ///< 只允許輸入數(shù)字
    LXDRestrictTypeOnlyDecimal = 2,     ///<  只允許輸入實(shí)數(shù)爱谁,包括.
    LXDRestrictTypeOnlyCharacter = 3,  ///<  只允許非中文輸入
};

在文本輸入的時(shí)候會(huì)有兩次回調(diào)晒喷,一次是代理的replace的替換文本方法,另一個(gè)需要我們手動(dòng)添加的EditingChanged編輯改變事件管行。前者在中文聯(lián)想輸入的時(shí)候無(wú)法準(zhǔn)確獲取文本內(nèi)容厨埋,而當(dāng)確認(rèn)好輸入的文本之后才會(huì)調(diào)用后面一個(gè)事件,因此回調(diào)后一個(gè)事件才能準(zhǔn)確的篩選文本捐顷。下面的代碼會(huì)篩選掉文本中所有的非數(shù)字:

- (void)viewDidLoad
{
    [textField addTarget: self action: @selector(textDidChanged:) forControlEvents: UIControlEventEditingChanged];
}

- (void)textDidChanged: (UITextField *)textField
{
    NSMutableString * modifyText = textField.text.mutableCopy;
    for (NSInteger idx = 0; idx < modifyText.length; idx++) {
        NSString * subString = [modifyText substringWithRange: NSMakeRange(idx, 1)];
        // 使用正則表達(dá)式篩選
        NSString * matchExp = @"^\\d$";
        NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", matchExp];
        if ([predicate evaluateWithObject: subString]) {
            idx++;
        } else {
            [modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
        }
    }
}

限制擴(kuò)展

如果說(shuō)我們每次需要限制輸入的時(shí)候都加上這么一段代碼也是有夠糟的,那么如何將這個(gè)功能給封裝出來(lái)并且實(shí)現(xiàn)自定義的限制擴(kuò)展呢雨效?筆者通過(guò)工廠來(lái)完成這一個(gè)功能迅涮,每一種文本的限制對(duì)應(yīng)一個(gè)單獨(dú)的類。抽象提取出一個(gè)父類徽龟,只提供一個(gè)文本變化的實(shí)現(xiàn)接口和一個(gè)限制最長(zhǎng)輸入的NSUInteger整型屬性:

#pragma mark - h文件
@interface LXDTextRestrict : NSObject

@property (nonatomic, assign) NSUInteger maxLength;
@property (nonatomic, readonly) LXDRestrictType restrictType;

// 工廠
+ (instancetype)textRestrictWithRestrictType: (LXDRestrictType)restrictType;
// 子類實(shí)現(xiàn)來(lái)限制文本內(nèi)容
- (void)textDidChanged: (UITextField *)textField;

@end


#pragma mark - 繼承關(guān)系
@interface LXDTextRestrict ()

@property (nonatomic, readwrite) LXDRestrictType restrictType;

@end

@interface LXDNumberTextRestrict : LXDTextRestrict
@end

@interface LXDDecimalTextRestrict : LXDTextRestrict
@end

@interface LXDCharacterTextRestrict : LXDTextRestrict
@end

#pragma mark - 父類實(shí)現(xiàn)
@implementation LXDTextRestrict

+ (instancetype)textRestrictWithRestrictType: (LXDRestrictType)restrictType
{
    LXDTextRestrict * textRestrict;
    switch (restrictType) {
        case LXDRestrictTypeOnlyNumber:
            textRestrict = [[LXDNumberTextRestrict alloc] init];
            break;
        
        case LXDRestrictTypeOnlyDecimal:
            textRestrict = [[LXDDecimalTextRestrict alloc] init];
            break;
        
        case LXDRestrictTypeOnlyCharacter:
            textRestrict = [[LXDCharacterTextRestrict alloc] init];
            break;
        
        default:
            break;
    }
    textRestrict.maxLength = NSUIntegerMax;
    textRestrict.restrictType = restrictType;
    return textRestrict;
}

- (void)textDidChanged: (UITextField *)textField
{

}

@end

由于子類在篩選的過(guò)程中都存在遍歷字符串以及正則表達(dá)式驗(yàn)證的流程叮姑,把這一部分代碼邏輯給封裝起來(lái)。根據(jù)EOC的原則優(yōu)先使用static inline的內(nèi)聯(lián)函數(shù)而非宏定義:

typedef BOOL(^LXDStringFilter)(NSString * aString);
static inline NSString * kFilterString(NSString * handleString, LXDStringFilter subStringFilter)
{
    NSMutableString * modifyString = handleString.mutableCopy;
    for (NSInteger idx = 0; idx < modifyString.length;) {
        NSString * subString = [modifyString substringWithRange: NSMakeRange(idx, 1)];
        if (subStringFilter(subString)) {
            idx++;
        } else {
            [modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
        }
    }
    return modifyString;
}

static inline BOOL kMatchStringFormat(NSString * aString, NSString * matchFormat)
{
    NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", matchFormat];
    return [predicate evaluateWithObject: aString];
}


#pragma mark - 子類實(shí)現(xiàn)
@implementation LXDNumberTextRestrict

- (void)textDidChanged: (UITextField *)textField
{
    textField.text = kFilterString(textField.text, ^BOOL(NSString *aString) {
        return kMatchStringFormat(aString, @"^\\d$");
    });
}

@end

@implementation LXDDecimalTextRestrict

- (void)textDidChanged: (UITextField *)textField
{
    textField.text = kFilterString(textField.text, ^BOOL(NSString *aString) {
        return kMatchStringFormat(aString, @"^[0-9.]$");
    });
}

@end

@implementation LXDCharacterTextRestrict

- (void)textDidChanged: (UITextField *)textField
{
    textField.text = kFilterString(textField.text, ^BOOL(NSString *aString) {
        return kMatchStringFormat(aString, @"^[^[\\u4e00-\\u9fa5]]$");
    });
}

@end

有了文本限制的類据悔,那么接下來(lái)我們需要新建一個(gè)UITextField的分類來(lái)添加輸入限制的功能传透,主要新增三個(gè)屬性:

@interface UITextField (LXDRestrict)

/// 設(shè)置后生效
@property (nonatomic, assign) LXDRestrictType restrictType;
/// 文本最長(zhǎng)長(zhǎng)度
@property (nonatomic, assign) NSUInteger maxTextLength;
/// 設(shè)置自定義的文本限制
@property (nonatomic, strong) LXDTextRestrict * textRestrict;

@end

由于這些屬性是category中添加的,我們需要手動(dòng)生成gettersetter方法极颓,這里使用objc_associate的動(dòng)態(tài)綁定機(jī)制來(lái)實(shí)現(xiàn)朱盐。其中核心的方法實(shí)現(xiàn)如下:

- (void)setRestrictType: (LXDRestrictType)restrictType
{
    objc_setAssociatedObject(self, LXDRestrictTypeKey, @(restrictType), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    self.textRestrict = [LXDTextRestrict textRestrictWithRestrictType: restrictType];
}

- (void)setTextRestrict: (LXDTextRestrict *)textRestrict
{
    if (self.textRestrict) {
        [self removeTarget: self.text action: @selector(textDidChanged:) forControlEvents: UIControlEventEditingChanged];
    }
    textRestrict.maxLength = self.maxTextLength;
    [self addTarget: textRestrict action: @selector(textDidChanged:) forControlEvents: UIControlEventEditingChanged];
    objc_setAssociatedObject(self, LXDTextRestrictKey, textRestrict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

完成這些工作之后,只需要一句代碼就可以完成對(duì)UITextField的輸入限制:

self.textField.restrictType = LXDRestrictTypeOnlyDecimal;

自定義的限制

假如現(xiàn)在文本框限制只允許輸入emoji表情菠隆,上面三種枚舉都不存在我們的需求兵琳,這時(shí)候自定義一個(gè)子類來(lái)實(shí)現(xiàn)這個(gè)需求。

@interface LXDEmojiTextRestrict : LXDTextRestrict

@end

@implementation LXDEmojiTextRestrict

- (void)textDidChanged: (UITextField *)textField
{
    NSMutableString * modifyString = textField.text.mutableCopy;
    for (NSInteger idx = 0; idx < modifyString.length;) {
        NSString * subString = [modifyString substringWithRange: NSMakeRange(idx, 1)];
        NSString * emojiExp = @"^[\\ud83c\\udc00-\\ud83c\\udfff]|[\\ud83d\\udc00-\\ud83d\\udfff]|[\\u2600-\\u27ff]$";
        NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", emojiExp];
        if ([predicate evaluateWithObject: subString]) {
            idx++;
        } else {
            [modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
        }
    }
    textField.text = modifyString;
}

@end

代碼中的emoji的正則表達(dá)式還不全骇径,因此在實(shí)踐中很多的emoji點(diǎn)擊會(huì)被篩選掉躯肌。效果如下:

鍵盤(pán)遮蓋

另一個(gè)讓人頭疼的問(wèn)題就是輸入框被鍵盤(pán)遮擋。這里通過(guò)在category中添加鍵盤(pán)相關(guān)通知來(lái)完成移動(dòng)整個(gè)window破衔。其中通過(guò)下面這個(gè)方法獲取輸入框在keyWindow中的相對(duì)坐標(biāo):

- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view

我們給輸入框提供一個(gè)設(shè)置自動(dòng)適應(yīng)的接口:

@interface UITextField (LXDAdjust)

/// 自動(dòng)適應(yīng)
- (void)setAutoAdjust: (BOOL)autoAdjust;

@end

@implementation UITextField (LXDAdjust)

- (void)setAutoAdjust: (BOOL)autoAdjust
{
    if (autoAdjust) {
        [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillShow:) name: UIKeyboardWillShowNotification object: nil];
        [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillHide:) name: UIKeyboardWillHideNotification object: nil];
    } else {
        [[NSNotificationCenter defaultCenter] removeObserver: self];
    }
}

- (void)keyboardWillShow: (NSNotification *)notification
{
    if (self.isFirstResponder) {
        CGPoint relativePoint = [self convertPoint: CGPointZero toView: [UIApplication sharedApplication].keyWindow];
    
        CGFloat keyboardHeight = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
        CGFloat actualHeight = CGRectGetHeight(self.frame) + relativePoint.y + keyboardHeight;
        CGFloat overstep = actualHeight - CGRectGetHeight([UIScreen mainScreen].bounds) + 5;
        if (overstep > 0) {
            CGFloat duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
            CGRect frame = [UIScreen mainScreen].bounds;
            frame.origin.y -= overstep;
            [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
                [UIApplication sharedApplication].keyWindow.frame = frame;
            } completion: nil];
        }
    }
}

- (void)keyboardWillHide: (NSNotification *)notification
{
    if (self.isFirstResponder) {
        CGFloat duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
        CGRect frame = [UIScreen mainScreen].bounds;
        [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
            [UIApplication sharedApplication].keyWindow.frame = frame;
        } completion: nil];
    }
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver: self];
}

@end


如果項(xiàng)目中存在自定義的UITextField子類清女,那么上面代碼中的dealloc你應(yīng)該使用method_swillzing來(lái)實(shí)現(xiàn)釋放通知的作用

尾語(yǔ)

其實(shí)大多數(shù)時(shí)候,實(shí)現(xiàn)某些小細(xì)節(jié)功能只是很簡(jiǎn)單的一些代碼晰筛,但是需要我們?nèi)チ私馐录憫?yīng)的整套邏輯來(lái)更好的完成它嫡丙。另外拴袭,昨天給微信小程序刷屏了,我想對(duì)各位iOS開(kāi)發(fā)者說(shuō)與其當(dāng)心自己的飯碗是不是能保住迄沫,不如干好自己的活稻扬,順帶學(xué)點(diǎn)js適應(yīng)一下潮流才是王道。本文demo

關(guān)注我的文集iOS開(kāi)發(fā)來(lái)獲取筆者文章動(dòng)態(tài)(轉(zhuǎn)載請(qǐng)注明本文地址及作者)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末羊瘩,一起剝皮案震驚了整個(gè)濱河市泰佳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尘吗,老刑警劉巖逝她,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異睬捶,居然都是意外死亡黔宛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門擒贸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)臀晃,“玉大人,你說(shuō)我怎么就攤上這事介劫』胀铮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵座韵,是天一觀的道長(zhǎng)险绘。 經(jīng)常有香客問(wèn)我,道長(zhǎng)誉碴,這世上最難降的妖魔是什么宦棺? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮黔帕,結(jié)果婚禮上代咸,老公的妹妹穿的比我還像新娘。我一直安慰自己蹬屹,他們只是感情好侣背,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著慨默,像睡著了一般贩耐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上厦取,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天潮太,我揣著相機(jī)與錄音,去河邊找鬼。 笑死铡买,一個(gè)胖子當(dāng)著我的面吹牛更鲁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奇钞,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼澡为,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了景埃?” 一聲冷哼從身側(cè)響起媒至,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谷徙,沒(méi)想到半個(gè)月后拒啰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡完慧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年谋旦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屈尼。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡册着,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脾歧,到底是詐尸還是另有隱情指蚜,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布涨椒,位于F島的核電站,受9級(jí)特大地震影響绽媒,放射性物質(zhì)發(fā)生泄漏蚕冬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一是辕、第九天 我趴在偏房一處隱蔽的房頂上張望囤热。 院中可真熱鬧,春花似錦获三、人聲如沸旁蔼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)棺聊。三九已至,卻和暖如春贞谓,著一層夾襖步出監(jiān)牢的瞬間限佩,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祟同,地道東北人作喘。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晕城,于是被迫代替她去往敵國(guó)和親泞坦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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

  • 摘要: IOS文本輸入框的使用方法總結(jié)砖顷。 (1)---------------------------------...
    破夕_____________閱讀 1,904評(píng)論 0 4
  • 印象 2002年秋贰锁。 開(kāi)學(xué)第一天,第一次見(jiàn)你择吊, 白凈李根、安靜。 自習(xí)課几睛,扯淡房轿、斗嘴的紙條中, 還挺聰明所森、不太浮夸囱持。 ...
    cshengbing閱讀 179評(píng)論 0 1
  • 領(lǐng)導(dǎo)讓我找一款市面上比較好用的bug管理工具,要求就是簡(jiǎn)潔焕济,夠用就好纷妆,經(jīng)過(guò)篩查對(duì)比,最終找到了4款產(chǎn)品晴弃,都是輕量級(jí)...
    鄧先森_cd09閱讀 859評(píng)論 1 1
  • 快月考了上鞠!昨晚际邻,零零后說(shuō):我有一項(xiàng)在全年級(jí)女生里應(yīng)該是最好的。我不自主地開(kāi)始上“”軌道“”芍阎,什么呢世曾?體育!她開(kāi)始從...
    一只愛(ài)龍的狗閱讀 217評(píng)論 0 0
  • 愛(ài)情開(kāi)始的城 2牧慕 我的大學(xué)谴咸,幾乎大部分時(shí)間都是晝伏夜出轮听。我也很期待有什么可以拯救我,讓我也為一些目標(biāo)而活著岭佳。就...
    草原上的咩咩羊閱讀 296評(píng)論 0 0