Обнаружение, если пользователь набрал символ emoji в UITextView
У меня есть UITextView, и мне нужно определить, входит ли пользователь в символ эмози.
Я бы подумал, что просто проверка значения unicode самого нового символа будет достаточной, но с новыми emoji 2s, некоторые символы будут разбросаны по всему индексу юникода (т.е. недавно созданные логотипы авторских прав и регистрации).
Возможно, что-то связано с проверкой языка символа с значениями NSLocale или LocalizedString?
Кто-нибудь знает хорошее решение?
Спасибо!
Ответы
Ответ 1
На протяжении многих лет эти решения для обнаружения эмоций продолжают ломаться, когда Apple добавляет новые эмоции с новыми методами (например, эмулятор эмоций, созданный путем проклятия персонажа с дополнительным символом) и т.д.
Я, наконец, сломался и просто написал следующий метод, который работает для всех текущих emojis и должен работать для всех будущих emojis.
Решение создает UILabel с символом и черным фоном. Затем CG берет снимок метки, и я просматриваю все пиксели в снимке для любых не сплошных черных пикселей. Причина, по которой я добавляю черный фон, заключается в том, чтобы избежать проблем с ложной раскраской из-за Subpixel Rendering
Решение работает очень быстро на моем устройстве, я могу проверить сотни символов в секунду, но следует отметить, что это решение 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;
}
Ответ 2
Другое решение: 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;
}
Надеюсь, что помогает;)
Ответ 3
Сначала позвольте обратиться к вашему "методу 55357" - и , почему он работает для многих символов emoji.
В Cocoa, a NSString
представляет собой набор из unichar
s, а unichar
- это только тип соответствия для unsigned short
, который совпадает с UInt16
. Поскольку максимальное значение UInt16
составляет 0xffff
, это исключает довольно много эмози из возможности вписаться в один unichar
, так как только два из шести основных блоков Unicode, используемых для emoji, попадают под этот диапазон:
Эти блоки содержат 113 emoji, а дополнительные 66 emoji, которые могут быть представлены как одиночные unichar
, могут быть найдены разбросанными по различным другим блокам. Однако эти 179 символов представляют только часть 1126 базовых символов emoji, остальная часть которых должна быть представлена более чем одним unichar
.
Проанализируйте свой код:
unichar unicodevalue = [text characterAtIndex:0];
Что происходит, так это то, что вы просто берете первую строку unichar
, и, хотя это работает для ранее упомянутых 179 символов, она разрывается, когда вы сталкиваетесь с символом UTF-32, поскольку NSString
преобразует все в кодировку UTF-16. Преобразование работает заменяя значение UTF-32 суррогатными парами, что означает, что NSString
теперь содержит два unichar
s.
И сейчас, мы получаем, почему число 55357 или 0xd83d
появляется для многих эможи: когда вы смотрите только на первое значение UTF-16 символа UTF-32, вы получить высокий суррогат, каждый из которых имеет диапазон 1024 низких суррогатов. Диапазон для высокого суррогата 0xd83d
равен U + 1F400-U + 1F7FF, который начинается в середине самого большого блока emoji, Различные символы и пиктограммы (U + 1F300-U + 1F5FF) и продолжается до Расширенные геометрические фигуры (U + 1F780-U + 1F7FF), содержащий в общей сложности 563 emoji и 333 символа неэможи в пределах этого диапазона.
Таким образом, впечатляющие 50% базовых символов emoji имеют высокий суррогат 0xd83d
, но эти методы дедукции по-прежнему оставляют 384 символа emoji необработанными, а также дают ложные срабатывания, по крайней мере, так много.
Итак, как вы можете определить, является ли символ эможи или нет?
Недавно я ответил на несколько смежный вопрос с реализацией Swift, и если вы захотите, вы можете посмотреть, как emoji обнаружены в эта структура, которую я создал с целью замены стандартных emoji на пользовательские изображения.
Во всяком случае, вы можете извлечь код кода 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
с вашим источником данных из кодов очков emoji, и вы точно узнаете, является ли этот символ эмози или нет.
P.S.
Есть несколько "невидимых" символов, а именно "Селектора вариаций" и стопоры нулевой ширины, которые также должны быть обработаны, поэтому я рекомендую изучить их, чтобы узнать, как они себя ведут.
Ответ 4
Если вы не хотите, чтобы ваша клавиатура отображала эможи, вы можете использовать
YOURTEXTFIELD/YOURTEXTVIEW.keyboardType = .ASCIICapable
Это покажет клавиатуру без emoji
Ответ 5
Вот метод обнаружения смайликов в 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
}
Ответ 6
Ниже приведены более чистые и эффективные реализации кода, который проверяет, имеет ли нарисованный символ какой-либо цвет или нет.
Они были написаны как методы категорий/расширений, чтобы их было проще использовать.
Objective-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
}
Swift:
Строка + 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
}
Ответ 7
Ну, вы можете определить, есть ли у него только символы ascii:
[myString canBeConvertedToEncoding:NSASCIIStringEncoding];
Он скажет "нет", если он терпит неудачу (или имеет эможи). Затем вы можете сделать оператор if else, который не позволяет им нажимать кнопку ввода или что-то в этом роде.
Ответ 8
Длина символов Emoji равна 2 и поэтому проверьте, является ли длина строки равным 2 в методе, которая должна должна быть: ShangeTextInRange: она вызывается после каждого нажатия клавиши на клавиатуре
- (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;
}
}