Почему эмодзи-символы вроде 👩👩👧👦 ведут себя странно в строках Swift?
Понимание Unicode и составных эмодзи в Swift
Вы столкнулись с фундаментальной особенностью Unicode, которую Swift отражает очень честно. Давайте разберёмся, что на самом деле представляет собой эмодзи 👩👩👧👦.
Этот эмодзи — не один символ в классическом понимании, а последовательность из 7 Unicode-скаляров, объединённых специальным символом-соединителем (Zero Width Joiner, U+200D). Вот его структура:
👩 (U+1F469) - женщина
(U+200D) - соединитель (ZWJ)
👩 (U+1F469) - женщина
(U+200D) - соединитель (ZWJ)
👧 (U+1F467) - девочка
(U+200D) - соединитель (ZWJ)
👦 (U+1F466) - мальчик
Когда вы видите family.count == 7, Swift сообщает вам количество Unicode-скаляров в строке. Это технически правильное значение, но не то, что вы ожидаете с точки зрения пользователя.
Что такое «символ» для пользователя?
В Swift есть понятие Extended Grapheme Cluster (расширенный кластер графемы) — это то, что пользователь воспринимает как один символ. Для вашего эмодзи это один кластер. Чтобы работать с ними, используйте свойства count у самой строки (оно считает именно кластеры) и метод first через индекс:
let family = "👩👩👧👦"
print(family.count) // 1 - это количество кластеров!
print(family.first) // Optional("👩👩👧👦") - весь кластер
// Правильный способ получить первый символ:
if let firstChar = family.first {
print(firstChar) // 👩👩👧👦
}
// Перебор по кластерам:
for char in family {
print(char) // 👩👩👧👦
}
Но подождите! Выше я сказал, что family.count == 7, а теперь говорю, что family.count == 1. В чём подвох? Всё дело в том, что count у String считает именно кластеры графем, а unicodeScalars.count считает скаляры. Проверьте:
print(family.unicodeScalars.count) // 7 - это скаляры
print(family.count) // 1 - это кластеры
Как безопасно обрезать строки с эмодзи?
Проблема обрезки возникает, когда вы используете prefix() или suffix() с количеством скаляров, а не кластеров. Вот правильный подход:
let text = "Привет 👩👩👧👦 мир! 🇷🇺"
// Плохо: обрезаем по скалярам
let badTruncation = String(text.unicodeScalars.prefix(10))
print(badTruncation) // "Привет 👩" - эмодзи разорван!
// Хорошо: обрезаем по кластерам
let goodTruncation = text.prefix(10)
print(goodTruncation) // "Привет 👩👩👧👦 м" - эмодзи цел!
// Если нужно обрезать до определённой длины в байтах (например, для API):
func safeTruncate(_ string: String, maxBytes: Int) -> String {
var result = ""
var currentBytes = 0
for char in string {
let charBytes = String(char).utf8.count
guard currentBytes + charBytes <= maxBytes else { break }
result.append(char)
currentBytes += charBytes
}
return result
}
print(safeTruncate(text, maxBytes: 20)) // "Привет 👩👩👩👧👦"
Практические советы
- Всегда используйте
countу строки, а не уunicodeScalars, если хотите считать «человеческие» символы. - Для итерации используйте
for char in string— это гарантирует, что вы получите целые кластеры. - Для индексации используйте
String.Index, а не целые числа. Например:text[text.index(text.startIndex, offsetBy: 5)]. - Будьте осторожны с флагами (🇷🇺 состоит из 2 скаляров) и эмодзи с тоном кожи (🧑🏻 — из 2 скаляров).
Помните: Swift выбрал честное представление Unicode в ущерб простоте. Это делает язык более надёжным при работе с интернациональными текстами, но требует понимания основ Unicode.
<<Альтернативный подход: использование NSString и Cocoa-методов
Если вам нужно быстрое решение без глубокого погружения в Unicode, можно использовать мосты к NSString и его методы, которые работают на уровне UTF-16. Однако помните, что это не решает проблему полностью — некоторые эмодзи всё ещё могут быть разорваны.
let family = "👩👩👧👦"
let nsString = family as NSString
// Количество "символов" по версии NSString (UTF-16)
print(nsString.length) // 11
// Получение подстроки (может разорвать эмодзи!)
let substring = nsString.substring(with: NSRange(location: 0, length: 5))
print(substring) // "👩👩" - разорвано!
// Более безопасный способ через enumerateSubstrings:
var safeChars: [String] = []
nsString.enumerateSubstrings(in: NSRange(location: 0, length: nsString.length),
options: .byComposedCharacterSequences) { substring, _, _, _ in
if let sub = substring { safeChars.append(sub) }
}
print(safeChars) // ["👩👩👧👦"] - один элемент
Этот подход может быть полезен при работе с legacy кодом или библиотеками, которые ожидают NSString. Но для нового кода я рекомендую использовать нативные Swift-методы, описанные в первом ответе — они более безопасны и производительны.