Почему эмодзи-последовательности, такие как 👩👩👧👦, ведут себя странно в строках Swift?
Понимание Unicode в Swift: графемные кластеры и составные эмодзи
Вы столкнулись с фундаментальной особенностью работы Swift со строками — графемными кластерами (grapheme clusters). Swift обрабатывает строки на уровне "воспринимаемых пользователем символов", а не отдельных кодовых точек Unicode. Это сделано для того, чтобы строки вели себя интуитивно понятно для людей, но иногда приводит к неожиданностям.
Почему ваш эмодзи считается одним символом?
Эмодзи 👩👩👧👦 — это не один символ, а последовательность из семи кодовых точек Unicode, объединённых с помощью нулевого соединителя (Zero Width Joiner, U+200D). Визуально это выглядит как одна картинка, но технически это:
👩 (U+1F469) + ZWJ (U+200D) + 👩 (U+1F469) + ZWJ (U+200D) + 👧 (U+1F467) + ZWJ (U+200D) + 👦 (U+1F466)
Swift рассматривает эту последовательность как один расширенный графемный кластер (extended grapheme cluster). Поэтому family.count возвращает 1 — это один "человеческий символ".
Как получить отдельные компоненты?
Чтобы разобрать составной эмодзи на части, нужно работать с разными уровнями представления строки:
let family = "👩👩👧👦"
// 1. Свойство count — количество графемных кластеров (воспринимаемых символов)
print("Графемные кластеры: \(family.count)") // 1
// 2. unicodeScalars — отдельные кодовые точки Unicode
print("Кодовые точки: \(family.unicodeScalars.count)") // 7
for scalar in family.unicodeScalars {
print(scalar, scalar.value)
}
// 3. utf8.count — количество байт в UTF-8
print("Байт в UTF-8: \(family.utf8.count)") // 25
// 4. utf16.count — количество 16-битных единиц (как в NSString)
print("16-битных единиц: \(family.utf16.count)") // 11
Практические рекомендации для работы с эмодзи
- Используйте
.countдля подсчёта видимых символов — это то, что видит пользователь. - Для итерации по символам используйте
for char in string— вы получите каждый графемный кластер. - Для доступа к отдельным компонентам (например, чтобы проверить, есть ли в эмодзи определённый элемент) работайте с
unicodeScalars. - Будьте осторожны с индексами:
string[0]не работает. Используйтеstring.index(string.startIndex, offsetBy: n).
Пример: проверка, содержит ли строка определённый эмодзи-компонент
func containsWoman(in emoji: String) -> Bool {
// Женщина: U+1F469
return emoji.unicodeScalars.contains(UnicodeScalar(0x1F469)!)
}
let family = "👩👩👧👦"
print(containsWoman(in: family)) // true
let single = "👦"
print(containsWoman(in: single)) // false
Почему Swift выбрал такой подход?
Это сделано для корректной работы с разными языками. Например, в корейском буквы объединяются в слоги, а в некоторых европейских языках используются комбинируемые диакритические знаки (например, "é" может быть как одним, так и двумя символами). Swift гарантирует, что .count всегда отражает то, что видит пользователь.
Если вам нужно работать с сырыми байтами (например, для отправки по сети), используйте .utf8 или .utf16. Для сравнения строк на равенство используйте обычный == — он учитывает графемные кластеры.
Альтернативный взгляд: работа с NSString для обратной совместимости
Если вы пришли из Objective-C или работаете с кодом, который ожидает поведение как в NSString, помните: Swift-строки мостуются к NSString. В отличие от Swift, NSString.length возвращает количество 16-битных единиц UTF-16, а не графемных кластеров:
import Foundation
let family = "👩👩👧👦"
let nsString = family as NSString
print("NSString length: \(nsString.length)") // 11 — не то, что вы ожидаете!
Это может вызвать ошибки, если вы смешиваете Swift и Objective-C API. Всегда используйте String.count для пользовательского интерфейса и String.utf16.count только если вам нужно точное соответствие с Objective-C.
Дополнительный совет: визуализация структуры эмодзи
Для отладки полезно увидеть, из каких частей состоит составной эмодзи. Используйте этот код:
func describeEmoji(_ emoji: String) {
print("Символ: \(emoji)")
print("Графем: \(emoji.count)")
print("Unicode скаляры:")
for scalar in emoji.unicodeScalars {
let isZWJ = scalar.value == 0x200D
print(" \(isZWJ ? "🔗 ZWJ" : "\(scalar) (U+\(String(format: "%04X", scalar.value)))")")
}
}
describeEmoji("👩👩👧👦")
Этот подход помогает понять, как Swift интерпретирует строку, и избежать ошибок при обработке пользовательского ввода, содержащего эмодзи.
Ответов пока нет.
Войдите чтобы оставить ответ