Something about UITextField

Posted by tooNight on Wed, 17 Jul 2019 19:59:18 +0200

From: http://sindrilin.com/ios-dev/2016/09/23/UITextField

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.

Topics: emoji iOS Mobile