Определить, набрал ли пользователь символ эмодзи в UITextView

У меня есть UITextView, и мне нужно определить, вводит ли пользователь символ смайликов.

Я бы подумал, что достаточно просто проверить значение юникода для новейшего символа, но с новыми смайликами 2 некоторые символы разбросаны по всему индексу юникода (например, недавно разработанные авторские права Apple и логотипы регистрации).

Возможно, что-то сделать с проверкой языка персонажа со значениями NSLocale или LocalizedString?

Кто-нибудь знает хорошее решение?

Спасибо!


person Albert Renshaw    schedule 15.01.2013    source источник
comment
Из любопытства, почему вы хотите обнаружить это?   -  person Jesse Rusak    schedule 15.01.2013
comment
Я делаю текстовый редактор, который добавляет текстовые эффекты с помощью HTML/CSS, но текст вводится через UITextField... Эмоджи не отображаются должным образом с моими эффектами CSS, поэтому мне нужно запретить пользователям их использовать.   -  person Albert Renshaw    schedule 15.01.2013
comment
Через 3 года можно будет добавить их в UILabel и посмотреть, назначен ли шрифт AppleColorEmoji? Вы также можете сделать снимок UILabel с символом и усреднить пиксели в один и посмотреть, черный ли он, если нет, то это смайлик (за исключением сплошных черных смайликов).   -  person Albert Renshaw    schedule 23.09.2016
comment
На самом деле, не добавляйте их в UILabel. Поместите их в NSMutableAttributedString, а затем вызовите для него .fixAttributes. Затем проверьте, какие шрифты им назначены. И вы можете проверить, не является ли это чем-то другим, кроме Helvetica: есть определенные символы, которые используют другие шрифты.   -  person MigMit    schedule 09.01.2021


Ответы (9)


На протяжении многих лет эти решения для обнаружения смайликов продолжают ломаться, поскольку Apple добавляет новые смайлики с новыми методами (например, смайлики с оттенком кожи, созданные путем предварительного проклятия персонажа дополнительным персонажем) и т. д.

В конце концов я сломался и просто написал следующий метод, который работает для всех текущих смайликов и должен работать для всех будущих смайликов.

Решение создает UILabel с символом и черным фоном. Затем компьютерная графика делает снимок этикетки, и я сканирую все пиксели на снимке на наличие не сплошных черных пикселей. Причина, по которой я добавляю черный фон, заключается в том, чтобы избежать проблем с ложным окрашиванием из-за субпиксельного рендеринга.

Решение работает ОЧЕНЬ быстро на моем устройстве, я могу проверять сотни символов в секунду, но следует отметить, что это решение CoreGraphics, и его не следует использовать интенсивно, как вы могли бы использовать обычный текстовый метод. Обработка графики требует больших объемов данных, поэтому одновременная проверка тысяч символов может привести к заметным задержкам.

-(BOOL)isEmoji:(NSString *)character {

    UILabel *characterRender = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
    characterRender.text = character;
    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;

}
person Albert Renshaw    schedule 23.01.2013
comment
Я никогда не видел решения, в котором создание жестко запрограммированного массива значений было бы хорошей идеей. Это предложение исключительно плохо тем, что оно изобилует ошибками и не рассчитано на будущее. Лучшим решением было бы комбинированное использование запроса textInputMode рассматриваемого UITextView и проверки того, является ли primaryLanguage смайликом. - person gschandler; 03.09.2014
comment
@gschandler Да, если только они не скопируют и не вставят смайлик или не используют пользовательскую клавиатуру (расширение приложения iOS8), которая имеет символ смайликов, но для основного языка установлен английский. - person Albert Renshaw; 04.09.2014
comment
Ваш второй блок if должен иметь else. Кроме того, к вашему сведению, вместо того, чтобы писать if(bool) { return NO; } еще { вернуть YES; } вы можете и захотите написать return !bool. - person moonman239; 24.07.2015
comment
Отредактировал мои старые решения и написал решение CG, не обращая внимания на предыдущие комментарии. - person Albert Renshaw; 24.02.2017
comment
@AlbertRenshaw Интересное решение, но я бы опубликовал его как совершенно новый ответ и вернул бы этот ответ к предыдущему состоянию. - person xoudini; 24.02.2017
comment
@AlbertRenshaw Возможно, вы захотите увидеть вики-ответ сообщества, который я опубликовал. Он обеспечивает гораздо более эффективную и чистую реализацию вашего кода (как в Objective-C, так и в Swift). Спасибо за отправную точку. - person rmaddy; 18.06.2019

Сначала давайте рассмотрим ваш "метод 55357" и почему он работает для многих символов эмодзи.

В Cocoa NSString — это набор unichar, а unichar — это просто псевдоним типа для unsigned short, который совпадает с UInt16. Поскольку максимальное значение UInt16 равно 0xffff, это исключает возможность размещения нескольких смайликов в одном unichar, поскольку только два из шести основных блоков Unicode, используемых для смайликов, попадают в этот диапазон:

Эти блоки содержат 113 смайликов, а дополнительные 66 смайликов, которые можно представить как один unichar, можно найти разбросанными по различным другим блокам. Однако эти 179 символов представляют лишь часть из 1126 базовых символов эмодзи, остальные должны быть представлены более чем одним unichar.

Давайте проанализируем ваш код:

unichar unicodevalue = [text characterAtIndex:0];

Происходит то, что вы просто берете первые unichar строки, и хотя это работает для ранее упомянутых 179 символов, оно распадается, когда вы сталкиваетесь с символом UTF-32, поскольку NSString преобразует все в кодировку UTF-16. Преобразование работает путем замены значения UTF-32 на суррогатные пары. , что означает, что NSString теперь содержит два unichar.

И теперь мы переходим к тому, почему число 55357 или 0xd83d используется для многих эмодзи: когда вы смотрите только на первое значение UTF-16 символа UTF-32, вы получаете старший суррогат, каждый из которых имеет диапазон 1024 младших суррогатов. Диапазон старшего суррогата 0xd83d — от U+1F400 до U+1F7FF, который начинается в середине самого большого блока эмодзи, Разные символы и пиктограммы (U+1F300–U+1F5FF) и продолжается вплоть до Расширенные геометрические фигуры (U+1F780–U+1F7FF) — содержит в общей сложности 563 смайлика и 333 символа, не являющегося смайликом, в этом диапазоне.

Таким образом, впечатляющие 50% базовых символов смайликов имеют высокий суррогат 0xd83d, но эти методы вывода по-прежнему оставляют необработанными 384 символа смайликов, а также дают ложные срабатывания как минимум для такого же количества.


Итак, как определить, является ли персонаж эмодзи или нет?

Недавно я ответил на вопрос, связанный с реализацией Swift, и если хотите, можете посмотреть, как обнаруживаются эмодзи. в этой структуре, которую я создал с целью замены стандартных эмодзи пользовательскими изображениями.

В любом случае, вы можете извлечь кодовую точку UTF-32 из символов, что мы и сделаем в соответствии с спецификация:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {

    // Get the UTF-16 representation of the text.
    unsigned long length = text.length;
    unichar buffer[length];
    [text getCharacters:buffer];

    // Initialize array to hold our UTF-32 values.
    NSMutableArray *array = [[NSMutableArray alloc] init];

    // Temporary stores for the UTF-32 and UTF-16 values.
    UTF32Char utf32 = 0;
    UTF16Char h16 = 0, l16 = 0;

    for (int i = 0; i < length; i++) {
        unichar surrogate = buffer[i];

        // High surrogate.
        if (0xd800 <= surrogate && surrogate <= 0xd83f) {
            h16 = surrogate;
            continue;
        }
        // Low surrogate.
        else if (0xdc00 <= surrogate && surrogate <= 0xdfff) {
            l16 = surrogate;

            // Convert surrogate pair to UTF-32 encoding.
            utf32 = ((h16 - 0xd800) << 10) + (l16 - 0xdc00) + 0x10000;
        }
        // Normal UTF-16.
        else {
            utf32 = surrogate;
        }

        // Add UTF-32 value to array.
        [array addObject:[NSNumber numberWithUnsignedInteger:utf32]];
    }

    NSLog(@"%@ contains values:", text);

    for (int i = 0; i < array.count; i++) {
        UTF32Char character = (UTF32Char)[[array objectAtIndex:i] unsignedIntegerValue];
        NSLog(@"\t- U+%x", character);
    }

    return YES;
}

Ввод «????» в UITextView выводит это на консоль:

???? contains values:
    - U+1f60e

Следуя этой логике, просто сравните значение character с вашим источником данных кодовых точек смайликов, и вы точно узнаете, является ли персонаж смайликом или нет.


P.S.

Есть несколько «невидимых» символов, а именно селекторы вариантов и объединители нулевой ширины, с которыми тоже нужно работать, поэтому я рекомендую изучить те, чтобы узнать, как они себя ведут.

person xoudini    schedule 07.12.2016
comment
Спасибо за такое подробное объяснение! Мне было интересно, как это все работает. Суррогатные пары - это интересно! Следует также отметить, что многие символы смайликов были добавлены в стандарт юникода с момента моего первоначального сообщения в 2013 году, тогда он учитывал почти все смайлики, которые, как мне кажется, за исключением, может быть, нескольких флагов. Я отмечу это как новый принятый ответ, еще раз спасибо! - person Albert Renshaw; 07.12.2016
comment
Нет проблем! И абсолютно точно, это, вероятно, сработало для большинства эмодзи, но также примите во внимание, что было бы отклонено несколько сотен символов, не являющихся эмодзи. Да, флаги состоят из двух комбинированных символов региональных индикаторов, поэтому все флаги будут выходить за пределы диапазона высокий суррогат 55357. - person xoudini; 07.12.2016

Другое решение: https://github.com/woxtu/NSString-RemoveEmoji

Затем, после импорта этого расширения, вы можете использовать его следующим образом:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
    // Detect if an Emoji is in the string "text"
    if(text.isIncludingEmoji) {
        // Show an UIAlertView, or whatever you want here
        return NO;
    }

    return YES;
}

Надеюсь, это поможет ;)

person Lapinou    schedule 08.01.2015
comment
Обратите внимание, что в iOS 9.1 добавлено больше смайликов, которые вышеупомянутый метод не распознает (особенно эти: ИСПРАВЛЕНИЕ: замените return (0x1d000 <= codepoint && codepoint <= 0x1f77f); в методе isEmoji на return (0x1d000 <= codepoint && codepoint <= 0x1f77f) || (0x1F900 <= codepoint && codepoint <=0x1f9ff); - person Vahan; 04.11.2015

если вы не хотите, чтобы на клавиатуре отображались смайлики, вы можете использовать YOURTEXTFIELD/YOURTEXTVIEW.keyboardType = .ASCIICapable
Это покажет клавиатуру без смайликов

person A_Curious_developer    schedule 02.06.2016
comment
Да, но пользователь по-прежнему может вставлять смайлики в - person Albert Renshaw; 02.06.2016

Вот метод обнаружения эмодзи в Swift. Это работает нормально. Надеюсь, это поможет другим.

 func isEmoji(_ character: String?) -> Bool {

        if character == "" || character == "\n" {
            return false
        }
        let characterRender = UILabel(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
        characterRender.text = character
        characterRender.backgroundColor = UIColor.black  
        characterRender.sizeToFit()
        let rect: CGRect = characterRender.bounds
        UIGraphicsBeginImageContextWithOptions(rect.size, true, 0.0)

        if let contextSnap:CGContext = UIGraphicsGetCurrentContext() {
            characterRender.layer.render(in: contextSnap)
        }

        let capturedImage: UIImage? = (UIGraphicsGetImageFromCurrentImageContext())
        UIGraphicsEndImageContext()
        var colorPixelFound:Bool = false

        let imageRef = capturedImage?.cgImage
        let width:Int = imageRef!.width
        let height:Int = imageRef!.height

        let colorSpace = CGColorSpaceCreateDeviceRGB()

        let rawData = calloc(width * height * 4, MemoryLayout<CUnsignedChar>.stride).assumingMemoryBound(to: CUnsignedChar.self)

            let bytesPerPixel:Int = 4
            let bytesPerRow:Int = bytesPerPixel * width
            let bitsPerComponent:Int = 8

            let context = CGContext(data: rawData, width: Int(width), height: Int(height), bitsPerComponent: Int(bitsPerComponent), bytesPerRow: Int(bytesPerRow), space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)



        context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height))

            var x:Int = 0
            var y:Int = 0
            while (y < height && !colorPixelFound) {

                while (x < width && !colorPixelFound) {

                    let byteIndex: UInt  = UInt((bytesPerRow * y) + x * bytesPerPixel)
                    let red = CGFloat(rawData[Int(byteIndex)])
                    let green = CGFloat(rawData[Int(byteIndex+1)])
                    let blue = CGFloat(rawData[Int(byteIndex + 2)])

                    var h: CGFloat = 0.0
                    var s: CGFloat = 0.0
                    var b: CGFloat = 0.0
                    var a: CGFloat = 0.0

                    var c = UIColor(red:red, green:green, blue:blue, alpha:1.0)
                    c.getHue(&h, saturation: &s, brightness: &b, alpha: &a)

                    b = b/255.0

                    if Double(b) > 0.0 {
                        colorPixelFound = true
                    }
                    x+=1
                }
                x=0
                y+=1
            }

        return colorPixelFound
}
person alamin39    schedule 30.04.2019
comment
Спасибо за преобразование! - person Albert Renshaw; 01.05.2019
comment
@alamin39 alamin39 Возможно, вы захотите увидеть вики-ответ сообщества, который я опубликовал. Он обеспечивает более эффективную и чистую версию. На самом деле в исходном коде Objective-C было несколько проблем, которые переносятся в ваш перевод. - person rmaddy; 18.06.2019

Ниже приведены более чистые и эффективные реализации кода, который проверяет, имеет ли нарисованный символ какой-либо цвет или нет.

Они были написаны как методы категории/расширения, чтобы упростить их использование.

Цель-C:

NSString+Emoji.h:

#import <Foundation/Foundation.h>

@interface NSString (Emoji)

- (BOOL)hasColor;

@end

NSString+Emoji.m:

#import "NSString+Emoji.h"
#import <UIKit/UIKit.h>

@implementation NSString (Emoji)

- (BOOL)hasColor {
    UILabel *characterRender = [[UILabel alloc] initWithFrame:CGRectZero];
    characterRender.text = self;
    characterRender.textColor = UIColor.blackColor;
    characterRender.backgroundColor = UIColor.blackColor;//needed to remove subpixel rendering colors
    [characterRender sizeToFit];

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

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

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

    BOOL result = NO;
    for (size_t offset = 0; offset < size; offset += bytesPerPixel) {
        unsigned char r = rawData[offset];
        unsigned char g = rawData[offset+1];
        unsigned char b = rawData[offset+2];

        if (r || g || b) {
            result = YES;
            break;
        }
    }

    free(rawData);

    return result;
}

@end

Пример использования:

if ([@"????" hasColor]) {
    // Yes, it does
}
if ([@"@" hasColor]) {
} else {
    // No, it does not
}

Свифт:

Строка+Emoji.swift:

import UIKit

extension String {
    func hasColor() -> Bool {
        let characterRender = UILabel(frame: .zero)
        characterRender.text = self
        characterRender.textColor = .black
        characterRender.backgroundColor = .black
        characterRender.sizeToFit()
        let rect = characterRender.bounds
        UIGraphicsBeginImageContextWithOptions(rect.size, true, 1)

        let contextSnap = UIGraphicsGetCurrentContext()!
        characterRender.layer.render(in: contextSnap)

        let capturedImageTmp = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        guard let capturedImage = capturedImageTmp else { return false }

        let imageRef = capturedImage.cgImage!
        let width = imageRef.width
        let height = imageRef.height

        let colorSpace = CGColorSpaceCreateDeviceRGB()

        let bytesPerPixel = 4
        let bytesPerRow = bytesPerPixel * width
        let bitsPerComponent = 8
        let size = width * height * bytesPerPixel
        let rawData = calloc(size, MemoryLayout<CUnsignedChar>.stride).assumingMemoryBound(to: CUnsignedChar.self)

        guard let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) else { return false }

        context.draw(imageRef, in: CGRect(x: 0, y: 0, width: width, height: height))

        var result = false
        for offset in stride(from: 0, to: size, by: 4) {
            let r = rawData[offset]
            let g = rawData[offset + 1]
            let b = rawData[offset + 2]

            if (r > 0 || g > 0 || b > 0) {
                result = true
                break
            }
        }

        free(rawData)

        return result
    }
}

Пример использования:

if "????".hasColor() {
    // Yes, it does
}
if "@".hasColor() {
} else {
    // No, it does not
}
person Community    schedule 18.06.2019

Тип Swift String имеет свойство .isEmoji.

Лучше всего проверить документацию на предостережение isEmojiPresentation

https://developer.apple.com/documentation/swift/unicode/scalar/properties/3081577-isemoji

person glotcha    schedule 27.11.2019
comment
Хотя это явный вопрос objective-c, я уверен, что он будет полезен многим посетителям, спасибо. - person Albert Renshaw; 27.11.2019
comment
С удовольствием, я был поражен, увидев эти снимки и проверив цветовые ответы также в Swift. С точки зрения Objc, возможно, стоит подумать о реструктуризации проектов для запуска в Swift и импорте кода Objc через связующий заголовок для доступа к некоторым из этих функций. - person glotcha; 27.11.2019

Ну, вы можете определить, есть ли у него только символы ascii, используя это:

[myString canBeConvertedToEncoding:NSASCIIStringEncoding];

Он скажет «нет», если он не работает (или имеет смайлики). Затем вы можете сделать оператор if else, который не позволяет им нажимать ввод или что-то в этом роде.

person Nate Lee    schedule 15.01.2013
comment
Я бы не стал этого делать. В основном любому неанглоязычному пользователю нужны диакритические знаки, и это не ASCII. Это приведет к большому количеству ложных срабатываний. - person zneak; 15.01.2013
comment
@zneak, тогда какой тип кодировки содержит диакритические знаки. В коде вы можете изменить NSASCIIStringEncoding на другую известную вам кодировку. - person Nate Lee; 15.01.2013
comment
эмодзи есть только в Юникоде, но в то же время только в Юникоде есть все символы всех языков. Не существует единой кодировки, в которой есть все символы, кроме эмодзи. Вот почему мне не нравится это решение. - person zneak; 15.01.2013
comment
@zneak Может быть, если вы сделаете что-то вроде оператора and, используя &&, и, возможно, у вас будет оператор or, используя int ||, это может сработать! (пример: ([myString canBeConvertedToEncoding:NSASCIIStringEncoding] || [myString canBeConvertedToEncoding:NSNSUTF8StringEncoding]) ) - person Nate Lee; 15.01.2013
comment
Все символы Unicode (включая Emoji) можно преобразовать в UTF-8, так что это не помогает. - person Martin R; 15.01.2013

Длина символов эмодзи равна 2, поэтому проверьте, равна ли длина строки 2 в методе shouldChangeTextInRange:, который вызывается после каждого нажатия клавиши на клавиатуре.

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text

{

    // Detect if an Emoji is in the string "text"
    if([text length]==2) {
        // Show an UIAlertView, or whatever you want here
        return YES;
    }
    else
{

       return NO;
}

} 
person muhammad kashif Jawad    schedule 28.01.2015
comment
Нет, не все символы эмодзи имеют длину 2, а также существует МНОГИЕ символы юникода с длиной 2, которые будут давать ложные срабатывания для этого. - person Albert Renshaw; 28.01.2015