解决方案iOS新手学习

iOS 输入框如何限制字符长度和emoji

2021-11-02  本文已影响0人  果哥爸

FJFTextInputIntercepter拦截器(通知)
FJFTextInputIntercepter拦截器(通知和代理)

我们经常会遇到这样的需求,比如手机输入框限制11位数字,个人简介最多不超过英文最多100个字,中文最多50个字,个人昵称不能使用emoji表情等等。

在上一篇文章了解了编码的基础上,我们来看下如何解决这些需求问题。

一. 如何判断emoji表情

我们可以知道emoji表情其实是由一个或多个Unicode编码点组成的字符串,而且emoji表情对应这一定的码元范围。

因此这里如果要判断一个字符串里面是否包含emoji表情,就要解决两个问题:

1.如何准确的将该字符串分为独立相关子字符串

在iOS中NSString可以通过enumerateSubstringsInRange:options:usingBlock:方法。这个方法把Unicode抽象的地方隐藏了,能让你更轻松的循环字符串里面的组合字符串,单词,行,句子,段落。

你甚至可以加上NSStringEnumerationLocalized这个选项,这样可以在确定词语间和句子间的边界时把用户所在区域考虑进去。要遍历单个字符,可以将参数指定为NSStringEnumerationByComposedCharacterSequences按字符顺序,依次遍历出相关子字符串。

这里表明了苹果想让我们把字符串看做子字符串的集合,因为:
1.单个unichar太小,不足以代表一个真正的Unicode字符。
2.一些字符由多个unicode码点组成。

2.如何判断子字符串是否为emoji

emoji表情对应着一定的码元范围,因此可以通过的判断字符的unicode编码来判断改字符是否为emoji编码。

+ (BOOL)validateContainsEmoji:(NSString *)string {
    __block BOOL returnValue = NO;
    [string enumerateSubstringsInRange:NSMakeRange(0, [string length])
                               options:NSStringEnumerationByComposedCharacterSequences
                            usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
        const unichar hs = [substring characterAtIndex:0];
        if (0xd800 <= hs && hs <= 0xdbff) {
            if (substring.length > 1) {
                const unichar ls = [substring characterAtIndex:1];
                const int uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
                if (0x1d000 <= uc && uc <= 0x1f77f) {
                    returnValue = YES;
                }
            }
        } else if (substring.length > 1) {
            const unichar ls = [substring characterAtIndex:1];
            if (ls == 0x20e3) {
                returnValue = YES;
            }
        } else {
            if (0x2100 <= hs && hs <= 0x27ff) {
                returnValue = YES;
            } else if (0x2B05 <= hs && hs <= 0x2b07) {
                returnValue = YES;
            } else if (0x2934 <= hs && hs <= 0x2935) {
                returnValue = YES;
            } else if (0x3297 <= hs && hs <= 0x3299) {
                returnValue = YES;
            } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50) {
                returnValue = YES;
            }
        }
    }];
    return returnValue;
}

但这里有个问题,就是emoji对应的码元范围会随着系统版本的而改变,因为每次版本更新可能会添加新的emoji表情,因此这个判断方法,需要一直更新,那有没有一种好的方法可以长期有效判断呢。

在我们长期的印象中,emoji表情都是带有色彩的,苹果键盘自带的emoji表情,从现在看来一直都是带有色彩的,而常规的文本一般都是黑色的,因此这里可以有如下解决方案:

具体实现如下:

+ (BOOL)fjf_stringContainsEmoji:(NSString *)string {
    //argument can be character or entire string
    UILabel *characterRender = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
    characterRender.text = string;
    characterRender.backgroundColor = [UIColor blackColor];//needed to remove subpixel rendering colors
    [characterRender sizeToFit];

    CGRect rect = [characterRender bounds];
    UIGraphicsBeginImageContextWithOptions(rect.size,YES,0.0f);
    CGContextRef contextSnap = UIGraphicsGetCurrentContext();
    [characterRender.layer renderInContext:contextSnap];
    UIImage *capturedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CGImageRef imageRef = [capturedImage CGImage];
    NSUInteger width = CGImageGetWidth(imageRef);
    NSUInteger height = CGImageGetHeight(imageRef);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    unsigned char *rawData = (unsigned char*) calloc(height * width * 4, sizeof(unsigned char));
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * width;
    NSUInteger bitsPerComponent = 8;
    CGContextRef context = CGBitmapContextCreate(rawData, width, height,
                                                 bitsPerComponent, bytesPerRow, colorSpace,
                                                 kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);

    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    CGContextRelease(context);

    BOOL colorPixelFound = NO;

    int x = 0;
    int y = 0;
    while (y < height && !colorPixelFound) {
        while (x < width && !colorPixelFound) {

            NSUInteger byteIndex = (bytesPerRow * y) + x * bytesPerPixel;

            CGFloat red = (CGFloat)rawData[byteIndex];
            CGFloat green = (CGFloat)rawData[byteIndex+1];
            CGFloat blue = (CGFloat)rawData[byteIndex+2];

            CGFloat h, s, b, a;
            UIColor *c = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
            [c getHue:&h saturation:&s brightness:&b alpha:&a];

            b /= 255.0f;

            if (b > 0) {
                colorPixelFound = YES;
            }
            x++;
        }
        x = 0;
        y++;
    }
    return colorPixelFound;
}

当然这个方法只适合对少量的字符串,因为如果字符串比较长,利用该方法进行解析判断会耗费CPU资源。

因此我们可以结合上面的emoji对应的码元范围和下面是否包含颜色来判断,对应字符串是否包含emoji表情,这样准确性会高点,但对于一些第三方的键盘如搜狗输入法里面的一些表情,还是不能很好过滤。

如果是swift语言,因为Swift 5.0,它带有一个新的Unicode.Scalar.Properties类,我们可以利用这个类的方法,向CharacterString类添加一些帮助属性。这里会:

extension String {
    /// 是否为单个emoji表情
    var isSingleEmoji: Bool {
        returncount==1&&containsEmoji
    }

    /// 包含emoji表情
    var containsEmoji: Bool {
        returncontains{ $0.isEmoji}
    }

    /// 只包含emoji表情
    var containsOnlyEmoji: Bool {
        return!isEmpty&&!contains{!$0.isEmoji}
    }

    /// 提取emoji表情字符串
    var emojiString: String {
        returnemojis.map{String($0) }.reduce("",+)
    }

    /// 提取emoji表情数组
    varemojis: [Character] {
        returnfilter{ $0.isEmoji}
    }

    /// 提取单元编码标量
    var emojiScalars: [UnicodeScalar] {
        returnfilter{ $0.isEmoji}.flatMap{ $0.unicodeScalars}
    }
}

extension Character {
    /// 简单的emoji是一个标量,以emoji的形式呈现给用户
    var isSimpleEmoji: Bool {
        guard let firstProperties = unicodeScalars.first?.properties else {
            return false
        }
        return unicodeScalars.count == 1 &&
            (firstProperties.isEmojiPresentation ||
                firstProperties.generalCategory == .otherSymbol)
    }

    /// 检查标量是否将合并到emoji中
    var isCombinedIntoEmoji: Bool {
        return unicodeScalars.count > 1 &&
            unicodeScalars.contains { $0.properties.isJoinControl || $0.properties.isVariationSelector }
    }

    /// 是否为emoji表情
    /// - Note: http://stackoverflow.com/questions/30757193/find-out-if-character-in-string-is-emoji
    var isEmoji: Bool {
        return isSimpleEmoji || isCombinedIntoEmoji
    }
}

同样swift上的该方法对于第三方键盘上的部分表情判断也没办法做到百分百准确。

二. 如何正确算出中英文字符串长度

比如一段个人简介中经常是禁止输入表情,但允许输入中英文,如果中文要算2个字符,英文算1个字符,如何准确的算出,该字符串的长度。

因为这里的汉字算2个字节,英文算1个字节,因此应该使用GB_18030_2000编码来计算字符串的长度。

GB_18030_2000主要有以下特点:

GB_18030_2000编码:

我们常用的汉字是3500个,都包含在双字节部分,因此使用GB_18030_2000来计算字符串长度可以完美的解决我们的需求。

三. 输入框拦截器(FJFTextInputIntercepter)

基于以上的知识,我写了一个输入框拦截器FJFTextInputIntercepter,该拦截器可以通过设置对应的参数来对输入框的输入进行限制:

// decimalPlaces 小数 位数
// (当intercepterNumberType 为FJFTextInputIntercepterNumberTypeDecimal 有用)
@property (nonatomic, assign) NSUInteger decimalPlaces;

// inputBlock 输入 回调处理
@property (nonatomic, copy) FJFTextInputIntercepterBlock inputBlock;

// beyoudLimitBlock 超过限制 最大 字符数 回调
@property (nonatomic, copy) FJFTextInputIntercepterBlock beyondLimitBlock;


// emojiAdmitted 是否 允许 输入 表情
@property (nonatomic, assign, getter=isEmojiAdmitted)   BOOL emojiAdmitted;

// intercepterNumberType 数字 类型
// FJFTextInputIntercepterNumberTypeNone 默认
// FJFTextInputIntercepterNumberTypeNumberOnly 只允许 输入 数字,emojiAdmitted,decimalPlaces 不起作用
// FJFTextInputIntercepterNumberTypeDecimal 分数 emojiAdmitted 不起作用 decimalPlaces 小数 位数
@property (nonatomic, assign) FJFTextInputIntercepterNumberType  intercepterNumberType;

/**
  doubleBytePerChineseCharacter 为 NO
 字母、数字、汉字都是1个字节 表情是两个字节
 doubleBytePerChineseCharacter 为 YES
 不允许 输入表情 一个汉字是否代表两个字节 default YES
 允许 输入表情 一个汉字代表3个字节 表情代表 4个字节
 */
@property (nonatomic, assign, getter=isDoubleBytePerChineseCharacter) BOOL doubleBytePerChineseCharacter;

这里我用了两种方式来写这个拦截器:

1. 通过输入框的delegate方法和输入框文本变化通知来拦截

#pragma mark - Delegate Methods
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    NSString *primaryLanguage = [textField.textInputMode primaryLanguage];
    
    return [self isAllowedInputWithReplaceRange:range replaceText:string previousText:textField.text primaryLanguage:primaryLanguage];
}

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    NSString *primaryLanguage = [textView.textInputMode primaryLanguage];
    return [self isAllowedInputWithReplaceRange:range replaceText:text previousText:textView.text primaryLanguage:primaryLanguage];
}

- (BOOL)isAllowedInputWithReplaceRange:(NSRange)replaceRange
                           replaceText:(NSString *)replaceText
                          previousText:(NSString *)previousText
                       primaryLanguage:(NSString *)primaryLanguage {

    NSString *newString = [previousText stringByReplacingCharactersInRange:replaceRange withString:replaceText];
    // 如果是删除 直接返回true
    if (newString.length < previousText.length) {
        return true;
    }
    
    // 是否 允许 输入
    if ([self isAllowedInputWithReplaceText:replaceText previousText:previousText primaryLanguage:primaryLanguage] == false) {
        return false;
    }
    
    // 是否 超出 限制
    if ([self isBeyondLimtWithInputText:newString]) {
        if (self.beyondLimitBlock) {
            self.beyondLimitBlock(self, previousText);
        }
        return false;
    }
    return true;
}

2. 只通过输入框文本变化通知来拦截

// 新添加的字符
- (NSString *)differentTextWithInputText:(NSString *)inputText
                            previousText:(NSString *)previousText {

    // 如果是删除 直接返回true
    if (inputText.length < previousText.length) {
        return @"";
    }
    
    NSString *differentText = nil;
    
    NSMutableArray <NSValue *> *inputSubMarray = [NSMutableArray array];
    NSMutableArray <NSValue *> *preSubMarray = [NSMutableArray array];

    [inputText enumerateSubstringsInRange:NSMakeRange(0, inputText.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
        [inputSubMarray addObject:[NSValue valueWithRange:substringRange]];
    }];
    
    [previousText enumerateSubstringsInRange:NSMakeRange(0, previousText.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
        [preSubMarray addObject:[NSValue valueWithRange:substringRange]];
    }];
    
    __block NSValue *startValue = nil;
    [inputSubMarray enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSRange subTextRange = [obj rangeValue];
        NSString *subText = [inputText substringWithRange:subTextRange];
        if (idx < preSubMarray.count) {
            NSRange preSubTextRange = [preSubMarray[idx] rangeValue];
            NSString *preSubText =  [previousText substringWithRange:preSubTextRange];
            if ([subText isEqualToString:preSubText] == false) {
                startValue = obj;
                *stop = true;
            }
        } else {
            startValue = obj;
            *stop = true;
        }
    }];
    
    NSRange startRange = [startValue rangeValue];
    if (startRange.location + startRange.length == inputText.length) {
        differentText = [inputText substringWithRange:startRange];
    } else {
        __block NSValue *endValue = nil;
        NSArray <NSValue *> *inputReverseSubArray = [[inputSubMarray reverseObjectEnumerator] allObjects];
        NSArray <NSValue *> *preReverseSubArray = [[preSubMarray reverseObjectEnumerator] allObjects];
        [preReverseSubArray enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSRange preTextRange = [obj rangeValue];
            NSString *preSubText = [previousText substringWithRange:preTextRange];
            NSValue *inputValue = inputReverseSubArray[idx];
            
            if (preTextRange.location >= startRange.location) {
                NSRange inputTextRange = [inputValue rangeValue];
                NSString *inputSubText =  [inputText substringWithRange:inputTextRange];
                if ([preSubText isEqualToString:inputSubText] == false) {
                    endValue = inputValue;
                    *stop = true;
                }
            } else {
                endValue = inputValue;
                *stop = true;
            }
        }];
        NSRange endRange = [endValue rangeValue];
        NSInteger differLength = endRange.location + endRange.length - startRange.location;
        NSRange differRange = NSMakeRange(startRange.location, differLength);
        differentText = [inputText substringWithRange:differRange];
    }
    
    return differentText;
}
- (void)updateTextViewWithTextView:(UITextView *)textView {
    NSString *inputText = textView.text;
    NSString *primaryLanguage = [textView.textInputMode primaryLanguage];

    NSInteger corsorStartPos = [textView offsetFromPosition:textView.beginningOfDocument toPosition:textView.selectedTextRange.start];
    
    // 如果 之前 文本 超出 字符限制
    if ([self isBeyondLimtWithInputText:self.previousText]) {
        textView.text = [self handleInputTextWithInputText:inputText];
        self.previousText = textView.text;
    }
    
    
    // 如果 当前字符串 小于 之前字符串(可能删除,也可能是特殊...造成)
    if (inputText.length < self.previousText.length) {
        if ([self isSpecialDotWithInputText:inputText previousText:self.previousText]) {
            NSInteger replaceTextLength =  self.previousText.length - inputText.length;
            textView.text = self.previousText;
            [FJFTextInputIntercepter cursorLocation:textView index:corsorStartPos + replaceTextLength];
        }
    }
    // 不允许 输入
    else if ([self isAllowedInputWithInputText:inputText previousText:self.previousText primaryLanguage:primaryLanguage] == false) {
        NSInteger replaceTextLength = inputText.length - self.previousText.length;
        textView.text = self.previousText;
        [FJFTextInputIntercepter cursorLocation:textView index:corsorStartPos - replaceTextLength];
    }

    
    self.previousText = textView.text;
    
    if (self.inputBlock) {
        self.inputBlock(self, textView.text);
    }
}

// 释放 是特殊的点点符号
- (BOOL)isSpecialDotWithInputText:(NSString *)inputText
                     previousText:(NSString *)previousText {
    // 如果 当前字符串 小于 之前输入字符串
    if (inputText.length < previousText.length) {
        NSString *replaceText = [self differentTextWithInputText:previousText previousText:inputText];
        if (replaceText.length > 1) {
            if (self.intercepterNumberType == FJFTextInputIntercepterNumberTypeDecimal ||
                self.intercepterNumberType == FJFTextInputIntercepterNumberTypeNumberOnly) {
                if ([inputText containsString:@"…"]) {
                    return true;
                }
            } else {
                __block BOOL isSpecialDot = true;
                [replaceText enumerateSubstringsInRange:NSMakeRange(0, replaceText.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
                    if ([substring isEqualToString:@"."] == false) {
                        isSpecialDot = false;
                        *stop = true;
                    }
                }];
                return isSpecialDot;
            }
        }
    }
    return false;
}

四.阅读延伸

Unicode与JavaScript详解
从Emoji的限制到Unicode编码

上一篇下一篇

猜你喜欢

热点阅读