字符和字形的关系可能会有些混乱。我们将深入探讨使用表情符号和Swift处理它们的方式。假设您要检查一个字符串是否包含一个或多个表情符号,你将如何处理?
背景
表情符号是电子消息和网页中使用的表意文字和笑脸。表情符号存在各种类型,包括面部表情,常见对象,天气的地点和类型以及动物。
尽管表情符号在2010年在全球范围内受到欢迎,但自1997年以来已经在日本使用。表情符号集最初由少于80个符号组成,现已增长到包含1200多个图标。
2010年也是将第一套Emoji添加到Unicode标准的一年。Unicode
是旨在统一处理和呈现文本的行业标准。它还包含来自世界各地的书写系统的字符索引,包括当前和古代的字符索引。该标准不断增长,版本12.1包含近138,000个字符。
该标准不仅包括来自世界各地的字母表中的字符,而且还包括看不见且不能单独使用的特殊字符。我们稍后再讨论。强烈建议您查看unicode-table.com,以了解其规模。只需向下滚动主页上的表格即可发现各种组合和可能性。
这是Unicode标准中定义的一些字符示例
深入
Unicode
标准定义的每个字符都有一个十六进制标识符(Unicode码),并且字符被分为块,例如希伯来语或阿拉伯语。
了解字符,字形和标量之间的区别很重要。Unicode
由Unicode
数字指定的字符组成。屏幕上可能不显示字符。同样,组合或字符可能会导致屏幕上出现一个字符。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 }
结果包含三个值:
- 十进制 49(十六进制U + 0031):普通的旧数字1
- 十进制65039(十六进制U + FE0F):变量选择器16
- 十进制8419(十六进制U + 20E3):组合封闭键帽
我们前面提到了那些特殊的标量。因此,这些字符的组合用于形成表情符号,将常规数字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)"})
最终结论
让我们结合这些信息,向Character
和String
类添加一些帮助属性。我们会:
- 检查一个字符是否恰好是将作为表情符号显示的一个标量
- 检查一个字符是否由多个标量组成,这些标量将被组合成一个表情符号。
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 ”)之类将被计为一个单独的字符。