iOS開發(fā)-UITextField的那點事

前言

UITextField被用作項目中獲取用戶信息的重要控件,但是在實際應(yīng)用中存在的不少的坑:修改keyboardType來限制鍵盤的類型,卻難以限制第三方鍵盤的輸入類型;在代理中限制了輸入長度以及輸入的文本類型,但是卻抵不住中文輸入的聯(lián)想;鍵盤彈起時遮住輸入框,需要接收鍵盤彈起收回的通知,然后計算坐標(biāo)實現(xiàn)移動動畫。

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

輸入限制

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

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

在文本輸入的時候會有兩次回調(diào),一次是代理的replace的替換文本方法,另一個需要我們手動添加的EditingChanged編輯改變事件。前者在中文聯(lián)想輸入的時候無法準(zhǔn)確獲取文本內(nèi)容,而當(dāng)確認(rèn)好輸入的文本之后才會調(diào)用后面一個事件,因此回調(diào)后一個事件才能準(zhǔn)確的篩選文本。下面的代碼會篩選掉文本中所有的非數(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ò)展

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

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

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

// 工廠
+ (instancetype)textRestrictWithRestrictType: (LXDRestrictType)restrictType;
// 子類實現(xiàn)來限制文本內(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 - 父類實現(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

由于子類在篩選的過程中都存在遍歷字符串以及正則表達(dá)式驗證的流程,把這一部分代碼邏輯給封裝起來。根據(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 - 子類實現(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

有了文本限制的類,那么接下來我們需要新建一個UITextField的分類來添加輸入限制的功能,主要新增三個屬性:

@interface UITextField (LXDRestrict)

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

@end

由于這些屬性是category中添加的,我們需要手動生成gettersetter方法,這里使用objc_associate的動態(tài)綁定機(jī)制來實現(xiàn)。其中核心的方法實現(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);
}

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

self.textField.restrictType = LXDRestrictTypeOnlyDecimal;

自定義的限制

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

@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á)式還不全,因此在實踐中很多的emoji點擊會被篩選掉。效果如下:

鍵盤遮蓋

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

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

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

@interface UITextField (LXDAdjust)

/// 自動適應(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


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

尾語

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 摘要: IOS文本輸入框的使用方法總結(jié)。 (1)---------------------------------...
    破夕_____________閱讀 2,094評論 0 4
  • 印象 2002年秋。 開學(xué)第一天,第一次見你, 白凈、安靜。 自習(xí)課,扯淡、斗嘴的紙條中, 還挺聰明、不太浮夸。 ...
    cshengbing閱讀 250評論 0 1
  • 領(lǐng)導(dǎo)讓我找一款市面上比較好用的bug管理工具,要求就是簡潔,夠用就好,經(jīng)過篩查對比,最終找到了4款產(chǎn)品,都是輕量級...
    鄧先森_cd09閱讀 959評論 1 1
  • 快月考了!昨晚,零零后說:我有一項在全年級女生里應(yīng)該是最好的。我不自主地開始上“”軌道“”,什么呢?體育!她開始從...
    一只愛龍的狗閱讀 274評論 0 0
  • 愛情開始的城 2牧慕 我的大學(xué),幾乎大部分時間都是晝伏夜出。我也很期待有什么可以拯救我,讓我也為一些目標(biāo)而活著。就...
    草原上的咩咩羊閱讀 343評論 0 0

友情鏈接更多精彩內(nèi)容