前言
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
中添加的,我們需要手動生成getter
和setter
方法,這里使用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開發來獲取筆者文章動態(轉載請注明本文地址及作者)