字符和字形的關(guān)系可能會有些混亂。我們將深入探討使用表情符號和Swift處理它們的方式。假設(shè)您要檢查一個字符串是否包含一個或多個表情符號,你將如何處理?
背景
表情符號是電子消息和網(wǎng)頁中使用的表意文字和笑臉。表情符號存在各種類型,包括面部表情,常見對象,天氣的地點和類型以及動物。
盡管表情符號在2010年在全球范圍內(nèi)受到歡迎,但自1997年以來已經(jīng)在日本使用。表情符號集最初由少于80個符號組成,現(xiàn)已增長到包含1200多個圖標。
2010年也是將第一套Emoji添加到Unicode標準的一年。Unicode
是旨在統(tǒng)一處理和呈現(xiàn)文本的行業(yè)標準。它還包含來自世界各地的書寫系統(tǒng)的字符索引,包括當前和古代的字符索引。該標準不斷增長,版本12.1包含近138,000個字符。
該標準不僅包括來自世界各地的字母表中的字符,而且還包括看不見且不能單獨使用的特殊字符。我們稍后再討論。強烈建議您查看unicode-table.com,以了解其規(guī)模。只需向下滾動主頁上的表格即可發(fā)現(xiàn)各種組合和可能性。
這是Unicode標準中定義的一些字符示例
深入
Unicode
標準定義的每個字符都有一個十六進制標識符(Unicode碼),并且字符被分為塊,例如希伯來語或阿拉伯語。
了解字符,字形和標量之間的區(qū)別很重要。Unicode
由Unicode
數(shù)字指定的字符組成。屏幕上可能不顯示字符。同樣,組合或字符可能會導致屏幕上出現(xiàn)一個字符。Swift通過對術(shù)語進行細微的區(qū)分來區(qū)分它們。這是一個非常復雜的故事,但要點是:
- 字符串由字符組成
- 字符由unicode標量組成
- 每個Unicode標量代表一個Unicode字符
回到Unicode字符。下面是一個例子:笑臉(??)被識別為U + 1F600并且是表情段的一部分。*你可以通過幾種方式在Swift字符串中表示表情符號:
let smiley1 = "??"
let smiley2 = "\u{1F600}" // Hex code, also "??"
看到這里,有人說“因此,我們可以找到表情符號的unicode
段,并檢查字符是否來自該段?”
然而,表情符號字符并不只有一個段,運輸和地圖,補充符號和象形文字有單獨的段,其他符號和象形文字中有很多圖標。
即使我們確定哪些段或哪些字符列表是emoji表情,也不是長久之計。該標準在不斷發(fā)展和擴展。
將此應用于代碼
在Swift 4.2及之前的版本中,我們一直在嘗試通過檢查Unicode
數(shù)字是否屬于預定義的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類,它為我們提供了一系列標志,以幫助我們弄清正在處理的內(nèi)容。我們可以很容易地獲取表示我們字符串的Unicode
標量數(shù)組。
下面舉個簡單的例子:
// 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
我們還沒有真正的成功。實際上還有一些字符由多個字形組成。看看我們?nèi)绾问褂?code>unicodeScalars.first?
請考參考以下示例:
"1??".unicodeScalars.first?.properties.isEmojiPresentation //false
"??".unicodeScalars.first?.properties.isEmojiPresentation //false
"????".unicodeScalars.first?.properties.isEmojiPresentation //true
"????????".unicodeScalars.first?.properties.isEmojiPresentation // true
為了解釋為什么會發(fā)生這種情況,讓我們看一下unicodeScalars屬性。該屬性unicodeScalars
返回的實例UnicodeScalarView
。它的debugDescription
只會產(chǎn)生原始的String,因此直接檢查內(nèi)容(或記錄它)并不能提供太多的見解。幸運的是,有一個map函數(shù)將返回一個常規(guī)數(shù)組,因此我們最終得到了一個元素數(shù)組:Unicode.Scalar
// 創(chuàng)建一個UnicodeScalarView
let scalarView = "1??".unicodeScalars
// 映射視圖,以便我們得到一個常規(guī)數(shù)組,可以檢查
let scalars = scalarView.map { $0 }
結(jié)果包含三個值:
我們前面提到了那些特殊的標量。因此,這些字符的組合用于形成表情符號,將常規(guī)數(shù)字1變成該符號。第二和第三標量修改了初始標量。
為了明確起見,您還可以使用十六進制unicode
標識符手動創(chuàng)建此組合:
“ \ 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" //組合成:①
小提示:也許您已經(jīng)看到在線的消息/文本看起來很混亂。這通常稱為Zalgo,實際上僅由許多Unicode字符組成,這些字符被合并為屏幕上的單個字符:
let lotsOfScalars =“ E??????????????”
let scalars = lotsOfScalars.unicodeScalars.map {$ 0}
// 合并為字符串,并添加空格以單獨查看它們
// //結(jié)果為: E ? ? ? ? ? ? ?? ? ? ? ? ? ?
scalarList = scalars.reduce("",{"\($ 0)\($ 1)"})
最終結(jié)論
讓我們結(jié)合這些信息,向Character
和String
類添加一些幫助屬性。我們會:
- 檢查一個字符是否恰好是將作為表情符號顯示的一個標量
- 檢查一個字符是否由多個標量組成,這些標量將被組合成一個表情符號。
extension Character {
/// 簡單的emoji是一個標量,以emoji的形式呈現(xiàn)給用戶
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表情數(shù)組
var emojis: [Character] {
returnfilter{ $0.isEmoji}
}
/// 提取單元編碼標量
var emojiScalars: [UnicodeScalar] {
returnfilter{ $0.isEmoji}.flatMap{ $0.unicodeScalars}
}
}
現(xiàn)在檢查我們的字符串中的表情符號變得非常簡單:
"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
總結(jié)一下
字符和標量之間有一個重要的區(qū)別。基本上,先定義標量的字符串,然后由系統(tǒng)渲染該字符串以確定標量將顯示哪些字符。
英文好的的童鞋可以看這里原文鏈接,雖然Unicode將每個代碼點定義為字符,但是Swift確實會調(diào)用這些標量,并將術(shù)語“字符”用于標量的組合,這可能會導致字符串中出現(xiàn)單個字形。我覺得,因此諸如控制字符(即“ null ”和“ backspace ”)之類將被計為一個單獨的字符。