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

字符和字形的關(guān)系可能會有些混亂浇坐。我們將深入探討使用表情符號和Swift處理它們的方式摇予。假設(shè)您要檢查一個字符串是否包含一個或多個表情符號,你將如何處理吗跋?

背景

表情符號是電子消息和網(wǎng)頁中使用的表意文字和笑臉。表情符號存在各種類型宁昭,包括面部表情跌宛,常見對象,天氣的地點和類型以及動物积仗。

盡管表情符號在2010年在全球范圍內(nèi)受到歡迎疆拘,但自1997年以來已經(jīng)在日本使用。表情符號集最初由少于80個符號組成寂曹,現(xiàn)已增長到包含1200多個圖標(biāo)哎迄。

2010年也是將第一套Emoji添加到Unicode標(biāo)準(zhǔn)的一年。Unicode是旨在統(tǒng)一處理和呈現(xiàn)文本的行業(yè)標(biāo)準(zhǔn)隆圆。它還包含來自世界各地的書寫系統(tǒng)的字符索引漱挚,包括當(dāng)前和古代的字符索引。該標(biāo)準(zhǔn)不斷增長渺氧,版本12.1包含近138,000個字符旨涝。

該標(biāo)準(zhǔn)不僅包括來自世界各地的字母表中的字符,而且還包括看不見且不能單獨使用的特殊字符侣背。我們稍后再討論白华。強烈建議您查看unicode-table.com,以了解其規(guī)模贩耐。只需向下滾動主頁上的表格即可發(fā)現(xiàn)各種組合和可能性弧腥。
這是Unicode標(biāo)準(zhǔn)中定義的一些字符示例

深入

Unicode標(biāo)準(zhǔn)定義的每個字符都有一個十六進制標(biāo)識符(Unicode碼),并且字符被分為塊潮太,例如希伯來語或阿拉伯語管搪。
了解字符,字形和標(biāo)量之間的區(qū)別很重要消别。UnicodeUnicode數(shù)字指定的字符組成抛蚤。屏幕上可能不顯示字符。同樣寻狂,組合或字符可能會導(dǎo)致屏幕上出現(xiàn)一個字符岁经。Swift通過對術(shù)語進行細微的區(qū)分來區(qū)分它們。這是一個非常復(fù)雜的故事蛇券,但要點是:

  • 字符串由字符組成
  • 字符由unicode標(biāo)量組成
  • 每個Unicode標(biāo)量代表一個Unicode字符

回到Unicode字符缀壤。下面是一個例子:笑臉(??)被識別為U + 1F600并且是表情段的一部分樊拓。*你可以通過幾種方式在Swift字符串中表示表情符號:

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

看到這里,有人說“因此塘慕,我們可以找到表情符號的unicode段筋夏,并檢查字符是否來自該段?”

然而图呢,表情符號字符并不只有一個段条篷,運輸和地圖補充符號和象形文字有單獨的段蛤织,其他符號和象形文字中有很多圖標(biāo)赴叹。
即使我們確定哪些段或哪些字符列表是emoji表情,也不是長久之計指蚜。該標(biāo)準(zhǔn)在不斷發(fā)展和擴展乞巧。

將此應(yīng)用于代碼

在Swift 4.2及之前的版本中,我們一直在嘗試通過檢查Unicode數(shù)字是否屬于預(yù)定義的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類,它為我們提供了一系列標(biāo)志免猾,以幫助我們弄清正在處理的內(nèi)容是辕。我們可以很容易地獲取表示我們字符串的Unicode標(biāo)量數(shù)組。
下面舉個簡單的例子:

// emoji表情
let smiley = "??"

//  獲取字符串標(biāo)量
let scalars = smiley.unicodeScalars // UnicodeScalarView instance

// 我們只有一個字符掸刊,因此我們用first得到他
let firstScalar = scalars.first // is 128512

// 注意128512實際上是1F600(十六進制)的十進制(??的unicode標(biāo)識)

// 獲取屬性
let properties = firstScalar?.properties

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

是不是這樣就可以了呢免糕?當(dāng)然不是,比如:

// 這里有個坑忧侧,這將會返回true
"3".unicodeScalars.first?.properties.isEmoji

這是因為標(biāo)量“ 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é)果包含三個值:

我們前面提到了那些特殊的標(biāo)量粤攒。因此所森,這些字符的組合用于形成表情符號,將常規(guī)數(shù)字1變成該符號夯接。第二和第三標(biāo)量修改了初始標(biāo)量必峰。
為了明確起見,您還可以使用十六進制unicode標(biāo)識符手動創(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}" //?? 

//豎起大拇指標(biāo)志:
" \ u {1F44D}" //?? 
//添加'表情符號修飾符Fitzpatrick Type-4':
" \ u {1F44D} \ u {1F3FD}" //???? 

//男人,女人凭需,女孩问欠,男孩
" \ u {1F468} \ u {1F469} \ u {1F467} \ u {1F466}" //???????? 
// 在每個標(biāo)量之間添加空格對應(yīng)標(biāo)量,這是將7個標(biāo)量組合成一個字符粒蜈。
" \ u {1F468} \ u {200D} \ u {1F469} \ u ” {200D} \ u {1F467} \ u {200D} \ u {1F466}" // ????????

最后顺献,請注意,并非每個由多個標(biāo)量組成的字符都是一個表情符號:

"\∪{} 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é)合這些信息,向CharacterString類添加一些幫助屬性蕊程。我們會:

  • 檢查一個字符是否恰好是將作為表情符號顯示的一個標(biāo)量
  • 檢查一個字符是否由多個標(biāo)量組成椒袍,這些標(biāo)量將被組合成一個表情符號。
extension Character {
    /// 簡單的emoji是一個標(biāo)量藻茂,以emoji的形式呈現(xiàn)給用戶
    var isSimpleEmoji: Bool {
        guardletfirstProperties = unicodeScalars.first?.propertieselse{
            returnfalse
        }
        return unicodeScalars.count == 1 &&
            (firstProperties.isEmojiPresentation||
                firstProperties.generalCategory==.otherSymbol)
    }

    /// 檢查標(biāo)量是否將合并到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}
    }

    /// 提取單元編碼標(biāo)量
    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é)一下

字符和標(biāo)量之間有一個重要的區(qū)別”娲停基本上优俘,先定義標(biāo)量的字符串,然后由系統(tǒng)渲染該字符串以確定標(biāo)量將顯示哪些字符掀序。

英文好的的童鞋可以看這里原文鏈接帆焕,雖然Unicode將每個代碼點定義為字符,但是Swift確實會調(diào)用這些標(biāo)量森枪,并將術(shù)語“字符”用于標(biāo)量的組合视搏,這可能會導(dǎo)致字符串中出現(xiàn)單個字形审孽。我覺得,因此諸如控制字符(即“ null ”和“ backspace ”)之類將被計為一個單獨的字符浑娜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末佑力,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子筋遭,更是在濱河造成了極大的恐慌打颤,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漓滔,死亡現(xiàn)場離奇詭異编饺,居然都是意外死亡,警方通過查閱死者的電腦和手機响驴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門透且,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人豁鲤,你說我怎么就攤上這事秽誊。” “怎么了琳骡?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵锅论,是天一觀的道長。 經(jīng)常有香客問我楣号,道長最易,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任炫狱,我火速辦了婚禮藻懒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘视译。我一直安慰自己束析,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布憎亚。 她就那樣靜靜地躺著员寇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪第美。 梳的紋絲不亂的頭發(fā)上蝶锋,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音什往,去河邊找鬼扳缕。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的躯舔。 我是一名探鬼主播驴剔,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼粥庄!你這毒婦竟也來了丧失?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤惜互,失蹤者是張志新(化名)和其女友劉穎布讹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體训堆,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡描验,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了坑鱼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膘流。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鲁沥,靈堂內(nèi)的尸體忽然破棺而出睡扬,到底是詐尸還是另有隱情,我是刑警寧澤黍析,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站屎开,受9級特大地震影響阐枣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奄抽,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一蔼两、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逞度,春花似錦额划、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至馆匿,卻和暖如春抑胎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背渐北。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工阿逃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓恃锉,卻偏偏與公主長得像搀菩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子破托,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345