swift5.0判断字符串中是否含有emoji表情的那些坑

字符和字形的关系可能会有些混乱。我们将深入探讨使用表情符号和Swift处理它们的方式。假设您要检查一个字符串是否包含一个或多个表情符号,你将如何处理?

背景

表情符号是电子消息和网页中使用的表意文字和笑脸。表情符号存在各种类型,包括面部表情,常见对象,天气的地点和类型以及动物。

尽管表情符号在2010年在全球范围内受到欢迎,但自1997年以来已经在日本使用。表情符号集最初由少于80个符号组成,现已增长到包含1200多个图标。

2010年也是将第一套Emoji添加到Unicode标准的一年。Unicode是旨在统一处理和呈现文本的行业标准。它还包含来自世界各地的书写系统的字符索引,包括当前和古代的字符索引。该标准不断增长,版本12.1包含近138,000个字符。

该标准不仅包括来自世界各地的字母表中的字符,而且还包括看不见且不能单独使用的特殊字符。我们稍后再讨论。强烈建议您查看unicode-table.com,以了解其规模。只需向下滚动主页上的表格即可发现各种组合和可能性。
这是Unicode标准中定义的一些字符示例

深入

Unicode标准定义的每个字符都有一个十六进制标识符(Unicode码),并且字符被分为块,例如希伯来语或阿拉伯语。
了解字符,字形和标量之间的区别很重要。UnicodeUnicode数字指定的字符组成。屏幕上可能不显示字符。同样,组合或字符可能会导致屏幕上出现一个字符。Swift通过对术语进行细微的区分来区分它们。这是一个非常复杂的故事,但要点是:

  • 字符串由字符组成
  • 字符由unicode标量组成
  • 每个Unicode标量代表一个Unicode字符

回到Unicode字符。下面是一个例子:笑脸(😀)被识别为U + 1F600并且是表情段的一部分。*你可以通过几种方式在Swift字符串中表示表情符号:

let smiley1 = "😀" 
let smiley2 = "\u{1F600}" // Hex code, also "😀"

看到这里,有人说“因此,我们可以找到表情符号的unicode段,并检查字符是否来自该段?”

然而,表情符号字符并不只有一个段,运输和地图补充符号和象形文字有单独的段,其他符号和象形文字中有很多图标。
即使我们确定哪些段或哪些字符列表是emoji表情,也不是长久之计。该标准在不断发展和扩展。

将此应用于代码

在Swift 4.2及之前的版本中,我们一直在尝试通过检查Unicode数字是否属于预定义的Unicode段之一来确定字符是否为表情符号。

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
    }
}

随之而来的是Swift 5.0,它带有一个新的Unicode.Scalar.Properties类,它为我们提供了一系列标志,以帮助我们弄清正在处理的内容。我们可以很容易地获取表示我们字符串的Unicode标量数组。
下面举个简单的例子:

// emoji表情
let smiley = "😀"

//  获取字符串标量
let scalars = smiley.unicodeScalars // UnicodeScalarView instance

// 我们只有一个字符,因此我们用first得到他
let firstScalar = scalars.first // is 128512

// 注意128512实际上是1F600(十六进制)的十进制(😀的unicode标识)

// 获取属性
let properties = firstScalar?.properties

// 检查它是不是emoji表情
let isEmoji = properties?.isEmoji // = true

是不是这样就可以了呢?当然不是,比如:

// 这里有个坑,这将会返回true
"3".unicodeScalars.first?.properties.isEmoji

这是因为标量“ 3” 可以表示为表情符号。属性isEmoji确实以这种方式引起误解。幸运的是,还有另一个属性:

// 这将像以前一样返回true:
"😀" .unicodeScalars.first?.properties.isEmojiPresentation
 
// 这将返回false,就像我们期望的那样:
"3" .unicodeScalars.first?.properties.isEmojiPresentation 

//不幸的是,这并不适用于所有表情符号:
"🌶" .unicodeScalars.first?.properties.isEmojiPresentation  //否
"🌶" .unicodeScalars.first?.properties.generalCategory == .some(.otherSymbol)// true

我们还没有真正的成功。实际上还有一些字符由多个字形组成。看看我们如何使用unicodeScalars.first?
请考参考以下示例:

"1️⃣".unicodeScalars.first?.properties.isEmojiPresentation //false
"♦️".unicodeScalars.first?.properties.isEmojiPresentation //false
"👍🏻".unicodeScalars.first?.properties.isEmojiPresentation //true
"👨👩👧👧".unicodeScalars.first?.properties.isEmojiPresentation // true

为了解释为什么会发生这种情况,让我们看一下unicodeScalars属性。该属性unicodeScalars返回的实例UnicodeScalarView。它的debugDescription只会产生原始的String,因此直接检查内容(或记录它)并不能提供太多的见解。幸运的是,有一个map函数将返回一个常规数组,因此我们最终得到了一个元素数组:Unicode.Scalar

// 创建一个UnicodeScalarView 
let scalarView = "1️⃣".unicodeScalars
// 映射视图,以便我们得到一个常规数组,可以检查
let scalars = scalarView.map { $0 }

结果包含三个值:

我们前面提到了那些特殊的标量。因此,这些字符的组合用于形成表情符号,将常规数字1变成该符号。第二和第三标量修改了初始标量。
为了明确起见,您还可以使用十六进制unicode标识符手动创建此组合:

“ \ u {0031}” //变成:1 
“ \ u {0031} \ u {20E3}” //变成:1⃣ 
“ \ u {0031} \ u {FE0F} \ u {20E3}” //变成:1️⃣

同样,其他表情符号可以组合:

//黑色钻石套装表情符号
" \ u {2666}" //♦ 
//添加'Variation Selector-16':
" \ u {2666} \ u {FE0F}" //♦️ 

//竖起大拇指标志:
" \ u {1F44D}" //👍 
//添加'表情符号修饰符Fitzpatrick Type-4':
" \ u {1F44D} \ u {1F3FD}" //👍🏽 

//男人,女人,女孩,男孩
" \ u {1F468} \ u {1F469} \ u {1F467} \ u {1F466}" //👨👩👧👦 
// 在每个标量之间添加空格对应标量,这是将7个标量组合成一个字符。
" \ u {1F468} \ u {200D} \ u {1F469} \ u ” {200D} \ u {1F467} \ u {200D} \ u {1F466}" // 👨👩👧👦

最后,请注意,并非每个由多个标量组成的字符都是一个表情符号:

"\∪{} 0061" //字母:a
"\∪{} 0302" //抑扬音: ^
"\∪{0061} \∪{} 0302" //组合成:①

小提示:也许您已经看到在线的消息/文本看起来很混乱。这通常称为Zalgo,实际上仅由许多Unicode字符组成,这些字符被合并为屏幕上的单个字符:

let lotsOfScalars =“ E̵͉͈̥̝͛͊̂͗͊̈́̄͜” 
let scalars = lotsOfScalars.unicodeScalars.map {$ 0} 
// 合并为字符串,并添加空格以单独查看它们
// //结果为: E  ̵  ͛  ͊  ̂  ͗  ͊  ̈́  ̄  ͜  ͉  ͈  ̥  ̝
scalarList = scalars.reduce("",{"\($ 0)\($ 1)"})

最终结论

让我们结合这些信息,向CharacterString类添加一些帮助属性。我们会:

  • 检查一个字符是否恰好是将作为表情符号显示的一个标量
  • 检查一个字符是否由多个标量组成,这些标量将被组合成一个表情符号。
extension Character {
    /// 简单的emoji是一个标量,以emoji的形式呈现给用户
    var isSimpleEmoji: Bool {
        guardletfirstProperties = unicodeScalars.first?.propertieselse{
            returnfalse
        }
        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
    varisEmoji:Bool{
        return isSimpleEmoji || isCombinedIntoEmoji
    }
}

接下来,我们将一些计算的属性添加到String来访问我们的Character扩展:

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

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

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

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

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

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

现在检查我们的字符串中的表情符号变得非常简单:

"A̛͚̖".containsEmoji // false
"3".containsEmoji // false
"A̛͚̖▶️".unicodeScalars // [65, 795, 858, 790, 9654, 65039]
"A̛͚̖▶️".emojiScalars // [9654, 65039]
"3️⃣".isSingleEmoji // true
"3️⃣".emojiScalars // [51, 65039, 8419]
"👌🏿".isSingleEmoji // true
"🙎🏼♂️".isSingleEmoji // true
"👨👩👧👧".isSingleEmoji // true
"👨👩👧👧".containsOnlyEmoji // true
"Hello 👨👩👧👧".containsOnlyEmoji // false
"Hello 👨👩👧👧".containsEmoji // true
"👫 Héllo 👨👩👧👧".emojiString // "👫👨👩👧👧"
"👨👩👧👧".count // 1
"👫 Héllœ 👨👩👧👧".emojiScalars // [128107, 128104, 8205, 128105, 8205, 128103, 8205, 128103]
"👫 Héllœ 👨👩👧👧".emojis // ["👫", "👨👩👧👧"]
"👫 Héllœ 👨👩👧👧".emojis.count // 2
"👫👨👩👧👧👨👨👦".isSingleEmoji // false
"👫👨👩👧👧👨👨👦".containsOnlyEmoji // true

总结一下

字符和标量之间有一个重要的区别。基本上,先定义标量的字符串,然后由系统渲染该字符串以确定标量将显示哪些字符。

英文好的的童鞋可以看这里原文链接,虽然Unicode将每个代码点定义为字符,但是Swift确实会调用这些标量,并将术语“字符”用于标量的组合,这可能会导致字符串中出现单个字形。我觉得,因此诸如控制字符(即“ null ”和“ backspace ”)之类将被计为一个单独的字符。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342