Почему эмодзи 👩‍👩‍👧‍👦 "ломают" строки в Swift? Разбор с кодом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

👩‍👩‍👧‍👦: Один символ, который сводит с ума

Представьте: вы пишете приложение для iOS, обрабатываете текст, считаете символы. Всё работает идеально, пока не встречается эмодзи семьи из трёх женщин и двух девочек. Вроде бы один символ на экране, но Swift говорит, что это... 4 символа? Или 7? А может, 11? Знакомая боль? Добро пожаловать в мир Unicode и Swift Strings!

Этот вопрос — один из самых популярных на Stack Overflow в разделе Swift. И не зря. Кажущаяся простота строк оборачивается настоящим квестом, когда в игру вступают составные эмодзи. В этой статье мы не просто объясним, почему так происходит, но и покажем, как правильно работать с такими строками в Swift. Если вы пишете на Swift и сталкиваетесь с обработкой пользовательского ввода, эмодзи или интернационализацией — этот материал для вас.

🔍 Проблема: "Сколько символов в этой строке?"

Давайте сразу к делу. Откройте Xcode Playground и напишите:

import Foundation

let familyEmoji = "👩‍👩‍‍👧‍👦"
print(familyEmoji.count) // 1? 4? 7?

Если вы ожидали 1, вы правы с точки зрения пользователя. Если вы ожидали 4 (четыре фигуры) — тоже логично. А Swift выдаст 1. Но попробуйте familyEmoji.unicodeScalars.count — и получите 7! А если разобрать на utf16.count — будет 11.

В чём подвох? Эмодзи 👩‍👩‍‍👧‍👦 — это составной графемный кластер. Он состоит из нескольких Unicode-скаляров, объединённых невидимыми символами-соединителями (ZWJ — Zero Width Joiner). Swift считает такие последовательности как один видимый символ (графему), что правильно с точки зрения пользователя, но сбивает с толку, когда вы пытаетесь работать с "внутренностями" строки.

🧱 Анатомия эмодзи: скаляры, кластеры и ZWJ

Чтобы понять магию, разберём наш эмодзи на составляющие. Каждый символ Unicode — это скаляр (числовой код). Для 👩‍👩‍‍👧‍👦 последовательность выглядит так:

let scalars = familyEmoji.unicodeScalars
for scalar in scalars {
    print(scalar.value, scalar.properties.name ?? "unknown")
}
// Вывод (упрощённо):
// 128105 (WOMAN)
// 8205 (ZERO WIDTH JOINER)
// 128105 (WOMAN)
// 8205 (ZERO WIDTH JOINER)
// 128103 (GIRL)
// 8205 (ZERO WIDTH JOINER)
// 128102 (BOY)

Видите? Четыре фигуры (две женщины, девочка, мальчик) соединены тремя невидимыми символами ZWJ (код 8205). Swift, следуя стандарту Unicode, трактует всю эту последовательность как один расширенный графемный кластер. Именно поэтому .count возвращает 1 — это количество "пользовательских символов".

🔄 Сравнение подходов: count vs unicodeScalars vs utf16

Давайте систематизируем. Для строки "👩‍👩‍👧‍👦" мы получаем три разных ответа в зависимости от того, какой API используем:

  • .count (графемные кластеры) — 1. Правильно для UI, отображения в UILabel.
  • .unicodeScalars.count (Unicode-скаляры) — 7. Полезно для низкоуровневой обработки текста.
  • .utf16.count (кодовые единицы UTF-16) — 11. Используется для совместимости с Objective-C NSString.
  • .utf8.count (байты UTF-8) — 25. Для работы с сетью и файлами.

Каждый из этих подходов решает свою задачу. Ошибка новичка — использовать .count для ограничения длины ввода в текстовом поле (например, "не более 140 символов"). Для пользователя "👩‍👩‍👧‍👦" — это один символ, и ограничение в 140 таких эмодзи — это нормально. Но если вы храните строку в базе данных с фиксированной длиной в байтах — вам нужен .utf8.count.

⚙️ Практические кейсы: как не обжечься

1. Ограничение длины в UITextField

Допустим, вы пишете твиттер-клиент и ограничиваете длину поста 280 символами. Используйте .count — он считает графемы. Пример:

let maxLength = 280
let userInput = "Привет! 👩‍👩‍👧‍👦 Как дела? 😊"
if userInput.count <= maxLength {
    print("Всё ок, \(userInput.count) символов")
} else {
    print("Слишком длинно: \(userInput.count) > \(maxLength)")
}

Это корректно. Но если вы захотите обрезать строку по индексу — будьте осторожны. Нельзя просто взять userInput[..<index], потому что индекс должен указывать на границу графемного кластера, иначе — краш.

2. Получение n-го символа

Попытка получить первый символ через string[0] в Swift не сработает — нужен String.Index. А для составных эмодзи это особенно важно:

let text = "👩‍👩‍👧‍👦💃"
let firstChar = text[text.startIndex] // 👩‍👩‍👧‍👦 (целиком!)
let secondChar = text[text.index(after: text.startIndex)] // 💃

Swift умный: он перескакивает через весь кластер за один шаг. Никаких половинок эмодзи.

3. Поиск подстроки внутри составного эмодзи

Хотите проверить, содержит ли строка женщину? Простое text.contains("👩") не сработает, потому что "👩" — это не полный графемный кластер в контексте ZWJ-последовательности. Придётся работать со скалярами:

let womanScalar: UnicodeScalar = "👩"
let containsWoman = familyEmoji.unicodeScalars.contains(womanScalar)
print(containsWoman) // true

Но помните: это не скажет вам, что женщина является частью семейного эмодзи. Для более сложного анализа (например, извлечения всех фигур) потребуется парсинг последовательности ZWJ.

🔬 Расширенные

Блоги

Рекомендаций по книгам