Je dois savoir si un personnage dans une chaîne est un emoji.
Par exemple, j'ai ce personnage:
let string = "????"
let character = Array(string)[0]
Je dois savoir si ce personnage est un emoji.
Ce que je suis tombé sur est la différence entre les caractères, les scalaires Unicode et les glyphes.
Par exemple, le glyphe ?????? ????? ???? se compose de 7 scalaires unicode:
Un autre exemple, le glyphe ???????? se compose de 2 scalaires unicode:
Ainsi, lors du rendu des caractères, les glyphes résultants importent vraiment.
Ce que je cherchais était un moyen de détecter si une chaîne est exactement et un seul emoji. Je pourrais donc le rendre plus gros que du texte normal (comme Messages le fait sur iOS10 et que WhatsApp le fait de nos jours). Comme décrit ci-dessus, le nombre de caractères est vraiment inutile. (Le "caractère de la colle" n'est pas non plus considéré comme un emoji).
Ce que vous pouvez faire, c'est utiliser CoreText pour vous aider à décomposer la chaîne en glyphes et à les compter. De plus, je déplacerais une partie de l'extension proposée par Arnold et Sebastian Lopez vers une extension séparée de UnicodeScalar
.
Cela vous laisse avec le résultat suivant:
import Foundation
extension UnicodeScalar {
/// Note: This method is part of Swift 5, so you can omit this.
/// See: https://developer.Apple.com/documentation/Swift/unicode/scalar
var isEmoji: Bool {
switch value {
case 0x1F600...0x1F64F, // Emoticons
0x1F300...0x1F5FF, // Misc Symbols and Pictographs
0x1F680...0x1F6FF, // Transport and Map
0x1F1E6...0x1F1FF, // Regional country flags
0x2600...0x26FF, // Misc symbols
0x2700...0x27BF, // Dingbats
0xE0020...0xE007F, // Tags
0xFE00...0xFE0F, // Variation Selectors
0x1F900...0x1F9FF, // Supplemental Symbols and Pictographs
0x1F018...0x1F270, // Various asian characters
0x238C...0x2454, // Misc items
0x20D0...0x20FF: // Combining Diacritical Marks for Symbols
return true
default: return false
}
}
var isZeroWidthJoiner: Bool {
return value == 8205
}
}
extension String {
// Not needed anymore in Swift 4.2 and later, using `.count` will give you the correct result
var glyphCount: Int {
let richText = NSAttributedString(string: self)
let line = CTLineCreateWithAttributedString(richText)
return CTLineGetGlyphCount(line)
}
var isSingleEmoji: Bool {
return glyphCount == 1 && containsEmoji
}
var containsEmoji: Bool {
return unicodeScalars.contains { $0.isEmoji }
}
var containsOnlyEmoji: Bool {
return !isEmpty
&& !unicodeScalars.contains(where: {
!$0.isEmoji && !$0.isZeroWidthJoiner
})
}
// The next tricks are mostly to demonstrate how tricky it can be to determine emoji's
// If anyone has suggestions how to improve this, please let me know
var emojiString: String {
return emojiScalars.map { String($0) }.reduce("", +)
}
var emojis: [String] {
var scalars: [[UnicodeScalar]] = []
var currentScalarSet: [UnicodeScalar] = []
var previousScalar: UnicodeScalar?
for scalar in emojiScalars {
if let prev = previousScalar, !prev.isZeroWidthJoiner, !scalar.isZeroWidthJoiner {
scalars.append(currentScalarSet)
currentScalarSet = []
}
currentScalarSet.append(scalar)
previousScalar = scalar
}
scalars.append(currentScalarSet)
return scalars.map { $0.map { String($0) }.reduce("", +) }
}
fileprivate var emojiScalars: [UnicodeScalar] {
var chars: [UnicodeScalar] = []
var previous: UnicodeScalar?
for cur in unicodeScalars {
if let previous = previous, previous.isZeroWidthJoiner, cur.isEmoji {
chars.append(previous)
chars.append(cur)
} else if cur.isEmoji {
chars.append(cur)
}
previous = cur
}
return chars
}
}
Ce qui vous donnera les résultats suivants:
"????????".isSingleEmoji // true
"????????♂️".isSingleEmoji // true
"????????????????".isSingleEmoji // true
"????????????????".containsOnlyEmoji // true
"Hello ????????????????".containsOnlyEmoji // false
"Hello ????????????????".containsEmoji // true
"???? Héllo ????????????????".emojiString // "????????????????????"
"????????????????".glyphCount // 1
"????????????????".characters.count // 4, Will return '1' in Swift 4.2 so previous method not needed anymore
"????????????????".count // 4, Will return '1' in Swift 4.2 so previous method not needed anymore
"???? Héllœ ????????????????".emojiScalars // [128107, 128104, 8205, 128105, 8205, 128103, 8205, 128103]
"???? Héllœ ????????????????".emojis // ["????", "????????????????"]
"????????????????????????????????".isSingleEmoji // false
"????????????????????????????????".containsOnlyEmoji // true
"????????????????????????????????".glyphCount // 3
"????????????????????????????????".characters.count // 8, Will return '3' in Swift 4.2 so previous method not needed anymore
Le moyen le plus simple, le plus propre et le plus rapide est simplement de vérifier les points de code Unicode de chaque caractère de la chaîne par rapport aux plages emoji et dingbats connues, comme suit:
extension String {
var containsEmoji: Bool {
for scalar in unicodeScalars {
switch scalar.value {
case 0x1F600...0x1F64F, // Emoticons
0x1F300...0x1F5FF, // Misc Symbols and Pictographs
0x1F680...0x1F6FF, // Transport and Map
0x2600...0x26FF, // Misc symbols
0x2700...0x27BF, // Dingbats
0xFE00...0xFE0F, // Variation Selectors
0x1F900...0x1F9FF, // Supplemental Symbols and Pictographs
0x1F1E6...0x1F1FF: // Flags
return true
default:
continue
}
}
return false
}
}
extension String {
func containsEmoji() -> Bool {
for scalar in unicodeScalars {
switch scalar.value {
case 0x3030, 0x00AE, 0x00A9,// Special Characters
0x1D000...0x1F77F, // Emoticons
0x2100...0x27BF, // Misc symbols and Dingbats
0xFE00...0xFE0F, // Variation Selectors
0x1F900...0x1F9FF: // Supplemental Symbols and Pictographs
return true
default:
continue
}
}
return false
}
}
Ceci est ma solution, avec des gammes mises à jour.
… Introduit une nouvelle façon de vérifier cela!
Vous devez diviser votre String
en son Scalars
. Chaque Scalar
a une valeur Property
qui prend en charge la valeur isEmoji
!
En fait, vous pouvez même vérifier si le scalaire est un modificateur Emoji ou plus. Consultez la documentation d'Apple: https://developer.Apple.com/documentation/Swift/unicode/scalar/properties
Vous pouvez envisager de rechercher isEmojiPresentation
au lieu de isEmoji
, car Apple indique ce qui suit pour isEmoji
:
Cette propriété est vraie pour les scalaires rendus par défaut sous la forme emoji, ainsi que pour ceux qui ont un rendu emoji autre que celui par défaut, suivis de U + FE0F VARIATION SELECTOR-16. Cela inclut certains scalaires qui ne sont généralement pas considérés comme des emoji.
De cette façon, les Emoji sont divisés en tous les modificateurs, mais leur gestion est beaucoup plus simple. Et comme Swift compte maintenant Emoji avec modificateurs (par exemple: ???? ???? ???? ????, ???????? ? ???, ????) en tant que vous pouvez faire toutes sortes de choses.
var string = "???? test"
for scalar in string.unicodeScalars {
let isEmoji = scalar.properties.isEmoji
print("\(scalar.description) \(isEmoji)"))
}
// ???? true
// false
// t false
// e false
// s false
// t false
NSHipster indique un moyen intéressant d'obtenir tous les Emoji:
import Foundation
var emoji = CharacterSet()
for codePoint in 0x0000...0x1F0000 {
guard let scalarValue = Unicode.Scalar(codePoint) else {
continue
}
// Implemented in Swift 5 (SE-0221)
// https://github.com/Apple/Swift-evolution/blob/master/proposals/0221-character-properties.md
if scalarValue.properties.isEmoji {
emoji.insert(scalarValue)
}
}
Swift 3 Note:
Il semble que le cnui_containsEmojiCharacters
La méthode a été supprimée ou déplacée vers une autre bibliothèque dynamique. _containsEmoji
devrait quand même fonctionner.
let str: NSString = "hello????"
@objc protocol NSStringPrivate {
func _containsEmoji() -> ObjCBool
}
let strPrivate = unsafeBitCast(str, to: NSStringPrivate.self)
strPrivate._containsEmoji() // true
str.value(forKey: "_containsEmoji") // 1
let swiftStr = "hello????"
(swiftStr as AnyObject).value(forKey: "_containsEmoji") // 1
Swift 2.x:
J'ai récemment découvert une API privée sur NSString
, qui expose des fonctionnalités permettant de détecter si une chaîne contient un caractère Emoji:
let str: NSString = "hello????"
Avec un protocole objc et unsafeBitCast
:
@objc protocol NSStringPrivate {
func cnui_containsEmojiCharacters() -> ObjCBool
func _containsEmoji() -> ObjCBool
}
let strPrivate = unsafeBitCast(str, NSStringPrivate.self)
strPrivate.cnui_containsEmojiCharacters() // true
strPrivate._containsEmoji() // true
Avec valueForKey
:
str.valueForKey("cnui_containsEmojiCharacters") // 1
str.valueForKey("_containsEmoji") // 1
Avec une chaîne pure Swift, vous devez la convertir en AnyObject
avant de pouvoir utiliser valueForKey
:
let str = "hello????"
(str as AnyObject).valueForKey("cnui_containsEmojiCharacters") // 1
(str as AnyObject).valueForKey("_containsEmoji") // 1
Méthodes trouvées dans le fichier d'en-tête NSString .
Pour Swift 3.0.2, la réponse suivante est la plus simple:
class func stringContainsEmoji (string : NSString) -> Bool
{
var returnValue: Bool = false
string.enumerateSubstrings(in: NSMakeRange(0, (string as NSString).length), options: NSString.EnumerationOptions.byComposedCharacterSequences) { (substring, substringRange, enclosingRange, stop) -> () in
let objCString:NSString = NSString(string:substring!)
let hs: unichar = objCString.character(at: 0)
if 0xd800 <= hs && hs <= 0xdbff
{
if objCString.length > 1
{
let ls: unichar = objCString.character(at: 1)
let step1: Int = Int((hs - 0xd800) * 0x400)
let step2: Int = Int(ls - 0xdc00)
let uc: Int = Int(step1 + step2 + 0x10000)
if 0x1d000 <= uc && uc <= 0x1f77f
{
returnValue = true
}
}
}
else if objCString.length > 1
{
let ls: unichar = objCString.character(at: 1)
if ls == 0x20e3
{
returnValue = true
}
}
else
{
if 0x2100 <= hs && hs <= 0x27ff
{
returnValue = true
}
else if 0x2b05 <= hs && hs <= 0x2b07
{
returnValue = true
}
else if 0x2934 <= hs && hs <= 0x2935
{
returnValue = true
}
else if 0x3297 <= hs && hs <= 0x3299
{
returnValue = true
}
else if hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50
{
returnValue = true
}
}
}
return returnValue;
}
La réponse absolument similaire à celle qui a été écrite avant moi, mais avec un ensemble de scalaires emoji mis à jour.
extension String {
func isContainEmoji() -> Bool {
let isContain = unicodeScalars.first(where: { $0.isEmoji }) != nil
return isContain
}
}
extension UnicodeScalar {
var isEmoji: Bool {
switch value {
case 0x1F600...0x1F64F,
0x1F300...0x1F5FF,
0x1F680...0x1F6FF,
0x1F1E6...0x1F1FF,
0x2600...0x26FF,
0x2700...0x27BF,
0xFE00...0xFE0F,
0x1F900...0x1F9FF,
65024...65039,
8400...8447,
9100...9300,
127000...127600:
return true
default:
return false
}
}
}
Au fil des ans, ces solutions de détection des emojis continuent à être utilisées Apple ajoute de nouveaux emojis avec de nouvelles méthodes (comme les emojis à la peau tonique construits en pré-maudissant un personnage avec un caractère supplémentaire), etc.
J'ai finalement craqué et juste écrit la méthode suivante qui fonctionne pour tous les emojis actuels et devrait fonctionner pour tous les futurs emojis.
La solution crée un UILabel avec le personnage et un arrière-plan noir. CG prend ensuite un instantané de l'étiquette et balaie tous les pixels de l'instantané à la recherche de pixels non noirs. La raison pour laquelle j’ajoute le fond noir est pour éviter les problèmes de fausse coloration dus à Subpixel Rendering
La solution tourne très vite sur mon appareil, je peux vérifier des centaines de caractères à la seconde, mais il convient de noter qu’il s’agit d’une solution CoreGraphics et qu’elle ne devrait pas être utilisée de la même manière que si vous utilisiez une méthode de texte ordinaire. Le traitement des graphiques est très chargé en données. La vérification de milliers de caractères à la fois peut entraîner un retard notable.
-(BOOL)isEmoji:(NSString *)character {
UILabel *characterRender = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
characterRender.text = character;
characterRender.font = [UIFont fontWithName:@"AppleColorEmoji" size:12.0f];//Note: Size 12 font is likely not crucial for this and the detector will probably still work at an even smaller font size, so if you needed to speed this checker up for serious performance you may test lowering this to a font size like 6.0
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;//Note: Alpha Channel not really needed, if you need to speed this up for serious performance you can refactor this pixel scanner to just RGB
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];//Note: I wrote this method years ago, can't remember why I check HSB instead of just checking r,g,b==0; Upon further review this step might not be needed, but I haven't tested to confirm yet.
b /= 255.0f;
if (b > 0) {
colorPixelFound = YES;
}
x++;
}
x=0;
y++;
}
return colorPixelFound;
}
Vous pouvez utiliser ce code exemple ou this pod .
Pour l'utiliser dans Swift, importez la catégorie dans le fichier YourProject_Bridging_Header
#import "NSString+EMOEmoji.h"
Ensuite, vous pouvez vérifier la plage de chaque émoticône de votre chaîne:
let example: NSString = "string????????????????with????emojis✊????" //string with emojis
let containsEmoji: Bool = example.Emo_containsEmoji()
print(containsEmoji)
// Output: ["true"]
J'ai créé un petit exemple de projet avec le code ci-dessus.
Vous pouvez utiliser NSString-RemoveEmoji comme ceci:
if string.isIncludingEmoji {
}
Avec Swift 5, vous pouvez maintenant inspecter les propriétés unicode de chaque caractère de votre chaîne. Cela nous donne la variable pratique isEmoji
de chaque lettre. Le problème est isEmoji
retournera vrai pour tout caractère pouvant être converti en emoji sur 2 octets, tel que 0-9.
Nous pouvons examiner la variable isEmoji
et également vérifier la présence d’un modificateur d’emoji pour déterminer si les caractères ambigus s’afficheront sous la forme d’un emoji.
Cette solution devrait être beaucoup plus durable que les solutions de regex proposées ici.
extension String {
func containsOnlyEmojis() -> Bool {
if count == 0 {
return false
}
for character in self {
if !character.isEmoji {
return false
}
}
return true
}
func containsEmoji() -> Bool {
for character in self {
if character.isEmoji {
return true
}
}
return false
}
}
extension Character {
// An emoji can either be a 2 byte unicode character or a normal UTF8 character with an emoji modifier
// appended as is the case with 3️⃣. 0x238C is the first instance of UTF16 emoji that requires no modifier.
// `isEmoji` will evaluate to true for any character that can be turned into an emoji by adding a modifier
// such as the digit "3". To avoid this we confirm that any character below 0x238C has an emoji modifier attached
var isEmoji: Bool {
guard let scalar = unicodeScalars.first else { return false }
return scalar.properties.isEmoji && (scalar.value > 0x238C || unicodeScalars.count > 1)
}
}
Nous donnant
"hey".containsEmoji() //false
"Hello World ????".containsEmoji() //true
"Hello World ????".containsOnlyEmojis() //false
"????".containsEmoji() //true
"????".containsOnlyEmojis() //true
Comptes pour:
Tout vrai:
"????".isPureEmojiString(withMinLength: 1, max: 1) // 1 scalar
"????????".isPureEmojiString(withMinLength: 1, max: 1) // 2 scalars (emoji modifier)
"⛄️".isPureEmojiString(withMinLength: 1, max: 1) // 2 scalars (variation selector)
"????????".isPureEmojiString(withMinLength: 1, max: 1) // 2 scalars (2x regional indicators)
"????♀️".isPureEmojiString(withMinLength: 1, max: 1) // 4 scalars (ZWJ + ♀ + variation)
"????????????????".isPureEmojiString(withMinLength: 1, max: 1) // 7 scalars (ZW joiners)
extension String {
func isPureEmojiString(withMinLength min: Int, max: Int) -> Bool {
if count < min || count > max {
return false
}
return isPureEmojiString()
}
func isPureEmojiString() -> Bool {
for scalar in unicodeScalars {
let prop = scalar.properties
if prop.isJoinControl || prop.isVariationSelector || prop.isEmojiModifier {
continue
}
else if scalar.properties.isEmoji == false || scalar.isASCII == true {
return false
}
}
return true
}
}