Find out if Character in String is emoji?

What I stumbled upon is the difference between characters, unicode scalars and glyphs.

For example, the glyph ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง consists of 7 unicode scalars:

  • Four emoji characters: ๐Ÿ‘จ๐Ÿ‘ฉ๐Ÿ‘ง๐Ÿ‘ง
  • In between each emoji is a special character, which works like character glue; see the specs for more info

Another example, the glyph ๐Ÿ‘Œ๐Ÿฟ consists of 2 unicode scalars:

  • The regular emoji: ๐Ÿ‘Œ
  • A skin tone modifier: ๐Ÿฟ

Last one, the glyph 1๏ธโƒฃ contains three unicode characters:

So when rendering the characters, the resulting glyphs really matter.

Swift 5.0 and above makes this process much easier and gets rid of some guesswork we needed to do. Unicode.Scalar‘s new Property type helps is determine what we’re dealing with.
However, those properties only make sense when checking the other scalars within the glyph. This is why we’ll be adding some convenience methods to the Character class to help us out.

For more detail, I wrote an article explaining how this works.

For Swift 5.0, this leaves you with the following result:

extension Character {
    /// A simple emoji is one scalar and presented to the user as an Emoji
    var isSimpleEmoji: Bool {
        guard let firstScalar = unicodeScalars.first else { return false }
        return firstScalar.properties.isEmoji && firstScalar.value > 0x238C
    }

    /// Checks if the scalars will be merged into an emoji
    var isCombinedIntoEmoji: Bool { unicodeScalars.count > 1 && unicodeScalars.first?.properties.isEmoji ?? false }

    var isEmoji: Bool { isSimpleEmoji || isCombinedIntoEmoji }
}

extension String {
    var isSingleEmoji: Bool { count == 1 && containsEmoji }

    var containsEmoji: Bool { contains { $0.isEmoji } }

    var containsOnlyEmoji: Bool { !isEmpty && !contains { !$0.isEmoji } }

    var emojiString: String { emojis.map { String($0) }.reduce("", +) }

    var emojis: [Character] { filter { $0.isEmoji } }

    var emojiScalars: [UnicodeScalar] { filter { $0.isEmoji }.flatMap { $0.unicodeScalars } }
}

Which will give you the following results:

"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
"โฐ".isSingleEmoji // true
"๐ŸŒถ".isSingleEmoji // true
"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง".isSingleEmoji // true
"๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ".isSingleEmoji // true
"๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ".containsOnlyEmoji // 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

For older Swift versions, check out this gist containing my old code.

Leave a Comment