前言
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)生成getter
和setter
方法极颓,這里使用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)注明本文地址及作者)