Определить, набрал ли пользователь символ Emoji в UITextView
У меня есть UITextView, и мне нужно определить, вводит ли пользователь символ смайлика.
Я думаю, что достаточно просто проверить значение Unicode для нового символа, но с новыми emoji 2s некоторые символы разбросаны по всему индексу Unicode (т. Е. Недавно разработанные Apple авторские права и логотипы регистрации).
Возможно, что-то связано с проверкой языка символа с помощью значений NSLocale или LocalizedString?
Кто-нибудь знает хорошее решение?
Спасибо!
6 ответов
С годами эти решения по обнаружению смайликов продолжают ломаться, поскольку Apple добавляет новые смайлики с новыми методами (например, смоделированные с помощью кожи смайлики, созданные путем предварительного проклятия персонажа дополнительным персонажем) и т. Д.
Я наконец сломался и просто написал следующий метод, который работает для всех текущих смайликов и должен работать для всех будущих смайликов.
Решение создает UILabel с символом и черным фоном. Затем CG делает снимок метки, и я сканирую все пиксели в снимке на наличие любых не сплошных черных пикселей. Причина, по которой я добавляю черный фон, состоит в том, чтобы избежать проблем с ложным окрашиванием из-за субпиксельного рендеринга.
Решение работает ОЧЕНЬ быстро на моем устройстве, я могу проверять сотни символов в секунду, но следует отметить, что это решение 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;
}
Сначала рассмотрим ваш "метод 55357" - и почему он работает для многих персонажей смайликов.
В какао NSString
это коллекция unichar
с и unichar
это просто typealias для unsigned short
который так же, как UInt16
, Поскольку максимальное значение UInt16
является 0xffff
, это исключает немало смайликов от возможности вписаться в один unichar
, поскольку только два из шести основных блоков Юникода, используемых для эмодзи, подпадают под этот диапазон:
- Разные символы (U+2600–U+26FF)
- Дингбаты (U+2700–U+27BF)
Эти блоки содержат 113 смайликов и еще 66 смайликов, которые можно представить как один unichar
можно найти разбросанные по различным другим блокам. Тем не менее, эти 179 символов представляют только часть из 1126 базовых символов эмодзи, остальные из которых должны быть представлены более чем одним 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, который начинается в середине самого большого блока смайликов, " Различные символы и пиктограммы" (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
к вашему источнику данных кодовых точек эмодзи, и вы будете точно знать, является ли персонаж эмодзи или нет.
PS
Есть несколько "невидимых" символов, а именно Селекторы вариаций и объединители нулевой ширины, которые также должны обрабатываться, поэтому я рекомендую изучить их, чтобы узнать, как они ведут себя.
Другое решение: 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;
}
Надеюсь, это поможет;)
Если вы не хотите, чтобы ваша клавиатура показывала эмодзи, вы можете использовать YOURTEXTFIELD/YOURTEXTVIEW.keyboardType = .ASCIICapable
Это покажет клавиатуру без Emoji
Вот метод обнаружения смайликов в 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
}
Ниже приведены более чистые и эффективные реализации кода, который проверяет, имеет ли нарисованный символ какой-либо цвет или нет.
Они были написаны как методы категорий / расширений, чтобы упростить их использование.
Цель-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
}
Тип Swift String имеет свойство.isEmoji
Лучше всего проверить документацию на предупреждение isEmojiPresentation
https://developer.apple.com/documentation/swift/unicode/scalar/properties/3081577-isemoji
Длина символов Emoji равна 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;
}
}
Хорошо, вы можете определить, есть ли у него только символы ascii, используя это:
[myString canBeConvertedToEncoding:NSASCIIStringEncoding];
Он скажет нет, если он потерпит неудачу (или имеет смайлики). Затем вы можете сделать оператор if else, который не позволяет им нажимать ввод или что-то еще.