UITextField is used as an important control to obtain user information in project, but there are many pits in practical application: modifying keyboard type to limit the type of keyboard, but it is difficult to limit the type of input of third-party keyboard; restricting the length of input and the type of text input in agent, but unable to resist Chinese input. Input association; when the keyboard pops up, it covers the input box. It needs to receive the notification of the keyboard pop-up and withdrawal, and then calculate the coordinates to achieve mobile animation.
For these problems, Apple does not provide a solution to the text input box, so this article will use category+runtime to solve these problems mentioned above. This article assumes that the reader has clearly become the first responder from UITextField to the end of the event call process in the editing process.
Input limitation
The most common input restrictions are mobile phone numbers and the amount of money, the former text can only exist in pure numbers, the latter text can also include decimal. The author temporarily defines three enumeration states to represent three text restrictions:
typedef NS_ENUM(NSInteger, LXDRestrictType)
{
LXDRestrictTypeOnlyNumber = 1, /// <Only numbers are allowed to be entered
LXDRestrictTypeOnlyDecimal = 2, /// <Only real numbers are allowed, including.
LXDRestrictTypeOnlyCharacter = 3, /// <Only non-Chinese input is allowed
};
There are two callbacks when text is entered, one is the replace ment text method of the agent, and the other is the EditingChanged edit change event that we need to add manually. The former can't get the text content accurately when Chinese associative input is made, but the latter event will be called only after confirming the input text, so the latter event can be filtered accurately after callback. The following code filters out all non-numerals in the text:
- (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)];
// Use regular expression filtering
NSString * matchExp = @"^\\d$";
NSPredicate * predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", matchExp];
if ([predicate evaluateWithObject: subString]) {
idx++;
} else {
[modifyString deleteCharactersInRange: NSMakeRange(idx, 1)];
}
}
}
Limit expansion
If it's bad enough to add such a piece of code every time we need to restrict input, how can we encapsulate this function and implement custom restriction extensions? The author completes this function through the factory, and the limitation of each text corresponds to a separate class. A parent class is abstracted, providing only an implementation interface for text changes and an NSUInteger integer property that restricts the longest input:
#pragma mark - h file
@interface LXDTextRestrict : NSObject
@property (nonatomic, assign) NSUInteger maxLength;
@property (nonatomic, readonly) LXDRestrictType restrictType;
// factory
+ (instancetype)textRestrictWithRestrictType: (LXDRestrictType)restrictType;
// Subclass implementation to restrict text content
- (void)textDidChanged: (UITextField *)textField;
@end
#pragma mark - inheritance relation
@interface LXDTextRestrict ()
@property (nonatomic, readwrite) LXDRestrictType restrictType;
@end
@interface LXDNumberTextRestrict : LXDTextRestrict
@end
@interface LXDDecimalTextRestrict : LXDTextRestrict
@end
@interface LXDCharacterTextRestrict : LXDTextRestrict
@end
#pragma mark - parent implementation
@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
Because subclasses have the process of traversing strings and validating regular expressions in the process of filtering, encapsulate this part of code logic. According to EOC principles, static inline inline functions are preferred over macro definitions:
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 - subclass implementation
@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
With text-restricted classes, then we need to create a new UITextField classification to add the function of input restriction, mainly adding three attributes:
@interface UITextField (LXDRestrict)
/// Effective after setup
@property (nonatomic, assign) LXDRestrictType restrictType;
/// Maximum length of text
@property (nonatomic, assign) NSUInteger maxTextLength;
/// Setting custom text restrictions
@property (nonatomic, strong) LXDTextRestrict * textRestrict;
@end
Since these attributes are added to category, we need to manually generate getter and setter methods, which are implemented using the dynamic binding mechanism of objc_association. The core methods are as follows:
- (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);
}
After all this work, it takes only one sentence of code to complete the input restrictions for UITextField:
self.textField.restrictType = LXDRestrictTypeOnlyDecimal;
Custom Limitations
If the current text box restriction only allows emoji expressions to be entered, none of the three enumerations above will meet our requirements, then we can customize a subclass to fulfill this requirement.
@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
The regular expression of emoji in the code is not complete, so in practice many emoji clicks are filtered out. The results are as follows:
Keyboard Cover
Another headache is that the input box is blocked by the keyboard. Here, the whole window is moved by adding keyboard-related notifications to the category. The relative coordinates of the input box in keyWindow are obtained by the following method:
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view
We provide an interface for setting up automatic adaptation for input boxes:
@interface UITextField (LXDAdjust)
/// Automatic adaptation
- (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
If there is a custom UITextField subclass in the project, then dealloc in the code above should use method_swillzing to release notifications
Concluding remarks
In fact, most of the time, the implementation of some small details function is only a very simple code, but we need to understand the whole logic of event response to better complete it. In addition, I brushed the screen for Wechat applet yesterday. I want to say to all iOS developers that it is better to do their work well than to be careful about whether their jobs can be saved. It is the kingdom to learn how to adapt js to the trend by the way. This article demo
For reprinting, please indicate the address and author of this article.