絵文字を含む文字列を見た目の文字数で評価して家族を守る記事


iOSだと絵文字を含む文字列の文字数を数える有効な手段が(調べた限り)ありません。
twitterでもバグが出ます。

あと1文字打てるので絵文字を一個加えたら文字数オーバーになってしまいました。

このような問題をうまく解決するにはどうすればいいのか、最近考えていて、一番マシな手法と思われるものができたのでまとめました。
改良点ありましたらコメントしていただければと思います。




extension String {

    var length: Int { return self.characters.count }

    var emojiVisibleLength: Int {
        var count = 0

        enumerateSubstringsInRange(startIndex ..< endIndex, options: .ByComposedCharacterSequences) { _ in
            count += 1
        }

        return count
    }

    private func calculateLengthWithEmoji(max: Int) -> Int {
        var possibleLengths: [Int] = []

        for subStringLength in min(length, emojiVisibleLength, max)...max*4 {
            let lengthWithEmoji = self[startIndex..<startIndex.advancedBy(subStringLength)].emojiVisibleLength

            switch lengthWithEmoji {
            case let lengthWithEmoji where lengthWithEmoji < max:
                continue
            case let lengthWithEmoji where lengthWithEmoji == max:
                possibleLengths.append(subStringLength)
            case let lengthWithEmoji where lengthWithEmoji > max:
                if possibleLengths.isEmpty {
                    possibleLengths.insert(subStringLength, atIndex: 0)
                }
            default:
                FFAReportUtil.report(.Error, message: "\(lengthWithEmoji) is invalid")
            }

            if lengthWithEmoji == emojiVisibleLength {
                break
            }
        }
        return possibleLengths.last!
    }

    func truncateWithEmoji(length: Int, trailing: String? = "...") -> String {
        if emojiVisibleLength > length {
            let lengthWithEmoji = calculateLengthWithEmoji(length)
            return self[startIndex..<startIndex.advancedBy(lengthWithEmoji)] + trailing!
        } else {
            return self
        }
    }
}


emojiVisibleLengthという変数は絵文字を含む文字列の場合でも見た目の文字数を取得できるプロパティです。

"👨‍👩‍👧‍👦".characters.count //4
"👨‍👩‍👧‍".characters.count //3
"🐧".characters.count //1
"🇺🇸🇺🇸".characters.count //1

"👨‍👩‍👧‍👦".emojiVisibleLength //1
"👨‍👩‍👧‍".emojiVisibleLength //2  <- errorです
"🐧".emojiVisibleLength //1
"🇺🇸🇺🇸".emojiVisibleLength //2

絵文字は見た目は1文字なのにcharacters.countでは違う値をとる場合があります。

そこで、emojiVisibleLengthという変数は絵文字を含む文字列の場合でも見た目の文字数を取得できるプロパティです(ただし、上記のようなerrorも含んでいます。修正したい。)。

こちらの記事を参考にしました

しかし、絵文字を含む文字列をはじめから120字だけ抜き出して表示させたい時に困りました。
見た目で120字目がcharacters.countで言うところの何文字目のことなのかわからないので、見た目で120字表示させたい時にindexを指定することができないのです。。。

例えば、
var test = "hog🐧👨‍👩‍👧‍👦eh🐧g👨‍👩‍👧‍👦ahkk🐧sjf"
を考えてみましょう。

a.5文字分だけ表示させたい場合
// "hog🐧👨‍👩‍👧‍👦"が欲しい

test[test.startIndex..<test.startIndex.advancedBy(5)] // "hog🐧👨‍"

なんか知らん奴が登場してきました!!!!!!!!!!!!
誰だ、このboyは!!!!!!!!!!!!!!!!
indexを一つ増やしてみましょう!

b.6文字分だけ表示させたい場合
// "hog🐧👨‍👩‍👧‍👦e"が欲しい

test[test.startIndex..<test.startIndex.advancedBy(6)] // "hog🐧👨‍👩‍"

ほ!!!

家族がバラバラにされています!!!!!!!!!
ということは、この流れだと7にすると父親が登場するかと思い、7にしたら、、、、

c.7文字分だけ表示させたい場合
// "hog🐧👨‍👩‍👧‍👦eh"が欲しい


test[test.startIndex..<test.startIndex.advancedBy(7)] // "hog🐧👨‍👩‍👧‍"



ふあ!!!!!!!!!
予想に反して家族が合流しました!!!!!!
ん、しかし、よく見ると、、、
あれ、子供がいない????!!!!!!!!!
この家族に何が起きたんdaaaaaaaaaaaaaaaaaaaaaa!!??????????




などと、混乱しました。

ちなみに続きはこんな感じ。

test[test.startIndex..<test.startIndex.advancedBy(8)] // "hog🐧👨‍👩‍👧‍👦"
test[test.startIndex..<test.startIndex.advancedBy(9)] // "hog🐧👨‍👩‍👧‍👦e"



8にするとようやく家族が元どおりになりました!!!

このように絵文字が入ることで、見た目での文字列の数とindexが全然合わなくなってしまいます。


そこで、「抜き出したい文字数を指定したら、その数だけ見た目上の文字数で抜き出した文字列のcharacters.countを算出する関数」を作成しました。


それがfunc calculateLengthWithEmoji(max: Int) -> Intです。

これはまだ未完成なのでご意見、ご提案ありましたら教えてください。

test.calculateLengthWithEmoji(5) //8
test.calculateLengthWithEmoji(6) //9
test.calculateLengthWithEmoji(7) //10
test.calculateLengthWithEmoji(8) //11
test.calculateLengthWithEmoji(9) //12



次に、上記で算出したcharacters.countを引数に抜き出した文字列の末尾のindexを取得することで、指定した文字数分だけ見た目上表示させる関数を作成しました。

それがfunc truncateWithEmoji(length: Int, trailing: String? = "...") -> Stringです。


test.truncateEmojiVisible(5) //"hog🐧👨‍👩‍👧‍👦..."
test.truncateEmojiVisible(6) //"hog🐧👨‍👩‍👧‍👦e..."
test.truncateEmojiVisible(7) //"hog🐧👨‍👩‍👧‍👦eh..."
test.truncateEmojiVisible(8) //"hog🐧👨‍👩‍👧‍👦eh🐧..."
test.truncateEmojiVisible(9) //"hog🐧👨‍👩‍👧‍👦eh🐧g..."


以上です。

開発者の思いやりが足らないせいで、知らないところで家族が一家離散してしまう、ということを実感しました。
みなさんのアプリでも同様の事件が起こらないように祈ります。

まだ改良中ですが、どなたかの参考になりましたら幸いです。