iOS開發-UITextField的那點事

前言

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

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

輸入限制

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

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

在文本輸入的時候會有兩次回調,一次是代理的replace的替換文本方法,另一個需要我們手動添加的EditingChanged編輯改變事件。前者在中文聯想輸入的時候無法準確獲取文本內容,而當確認好輸入的文本之后才會調用后面一個事件,因此回調后一個事件才能準確的篩選文本。下面的代碼會篩選掉文本中所有的非數字:

- (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)];
        // 使用正則表達式篩選
        NSString * matchExp = @"^\\d$";
        NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", matchExp];
        if ([predicate evaluateWithObject: subString]) {
            idx++;
        } else {
            [modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
        }
    }
}

限制擴展

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

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

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

// 工廠
+ (instancetype)textRestrictWithRestrictType: (LXDRestrictType)restrictType;
// 子類實現來限制文本內容
- (void)textDidChanged: (UITextField *)textField;

@end


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

@property (nonatomic, readwrite) LXDRestrictType restrictType;

@end

@interface LXDNumberTextRestrict : LXDTextRestrict
@end

@interface LXDDecimalTextRestrict : LXDTextRestrict
@end

@interface LXDCharacterTextRestrict : LXDTextRestrict
@end

#pragma mark - 父類實現
@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

由于子類在篩選的過程中都存在遍歷字符串以及正則表達式驗證的流程,把這一部分代碼邏輯給封裝起來。根據EOC的原則優先使用static inline的內聯函數而非宏定義:

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 - 子類實現
@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)

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

@end

由于這些屬性是category中添加的,我們需要手動生成gettersetter方法,這里使用objc_associate的動態綁定機制來實現。其中核心的方法實現如下:

- (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;

自定義的限制

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

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

鍵盤遮蓋

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

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

我們給輸入框提供一個設置自動適應的接口:

@interface UITextField (LXDAdjust)

/// 自動適應
- (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你應該使用method_swillzing來實現釋放通知的作用

尾語

其實大多數時候,實現某些小細節功能只是很簡單的一些代碼,但是需要我們去了解事件響應的整套邏輯來更好的完成它。另外,昨天給微信小程序刷屏了,我想對各位iOS開發者說與其當心自己的飯碗是不是能保住,不如干好自己的活,順帶學點js適應一下潮流才是王道。本文demo

關注我的文集iOS開發來獲取筆者文章動態(轉載請注明本文地址及作者)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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