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

字符和字形的關(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ū)別很重要。UnicodeUnicode數(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é)合這些信息,向CharacterString類添加一些幫助屬性。我們會:

  • 檢查一個字符是否恰好是將作為表情符號顯示的一個標量
  • 檢查一個字符是否由多個標量組成,這些標量將被組合成一個表情符號。
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 ”)之類將被計為一個單獨的字符。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。