Выбор эмодзи - обычное явление в приложениях, потому что они стали популярным способом самовыражения пользователей в цифровом виде. Они могут быть использованы для придания индивидуальности и эмоций текстовым сообщениям или дополнительной смысловой окраски различным элементам приложения.

К примеру, в моем приложении Timetable, которое предназначено для ведения учебного расписания, нужно было добавить возможность прикреплять смайлик к названию расписания.

UI настройки названия расписания
UI настройки названия расписания

Сделать это системным способом можно только с помощью UITextField или UITextViewкостыль с типом клавиатуры. В моем случае это не подходило и я начал искать другие способы. Нашел несколько библиотек, но все из них мне не подошли по разным причинам. Где-то не хватало локализации и были различные баги, где-то UI не подходил и так далее. Главное для меня было, чтобы этот элемент был в стилистике Apple, корректно работал и был локализован на все языки.

Подходящие решение нашел в MacOS. Там как раз есть системный элемент, который появляется в нужном тебе месте и имеет стрелочку, которая показывает какой элемент является отправной точкой.

Элемент выбора эмодзи в MacOS
Элемент выбора эмодзи в MacOS

В итоге принял решение сделать подобный элемент для iOS. Далее расскажу с какими трудностями столкнулся и какие решения использовал во время разработки.

Поиск списка эмодзи

Для начала стал искать упорядоченный список смайликов, который разбит по категориям. Сперва посмотрел в сторону системных решений от Apple, но, к сожалению, никаких списков они не дают. После этого, полез в интернет и сходу нашел кучу списков, но после, чуть более детального изучения, вылезла проблема с обратной совместимостью эмодзи.

То есть, эмодзи добавленный в iOS 15, на всех предыдущих версиях будет черным квадратом с вопросительным знаком по центру, а во всех подобных списках лежат просто массивы со строковыми значениями в виде эмодзи. Какие версии iOS они поддерживают - никто не знает ????????‍♂️

И если я хочу поддерживать не только последнюю версию iOS, мне нужно найти список, где указано когда был добавлен смайлик. Первая мысль была, что они так и разбиты по версиям iOS, но углубляясь вспомнил, что есть же еще и Android…

Оказалось, что эмодзи разделяются на свои Unicode версии:

Список Unicode версий эмодзи
Список Unicode версий эмодзи

Этот список находится на сайте - unicode.com. Там же и лежат нужные мне файлы со списками эмодзи, но там нет локализации, только английский язык ???? Но об этом позже.

emoji-test.txt файл для версии эмодзи 15.0
emoji-test.txt файл для версии эмодзи 15.0

В данном файле эмодзи состоят из одного или нескольких шестнадцатеричных значений. Для того, чтобы получить из этих значений сам эмодзи, необходимо преобразовать это в массив шестнадцатеричных значений и сделать с ними следующие:

print(
    [0x1F600]
      // Преобразуем шестнадцатеричное значение в 32-битное целочисленное представление нашего смайлика в таблице Unicode.
      .map({ UnicodeScalar($0) })
      // Убираем опционал.
      .compactMap({ $0 })
      // Преобразуем 32-битное целое число в символ для правильного представления.
      .map({ String($0) })
      // Объединяем все значения, чтобы получить окончательный смайлик.
      .joined()
) // "????"

После того как научился преобразовывать шестнадцетиричное представление смайлика в строку, мне нужно было конвертировать этот список в упорядоченный массив для взаимодействия с ним уже из кода.

Для того, чтобы из файла emoji-test.txt получить модель, быстро на коленке написал парсер, который проходился по строкам, разбивая их на нужные мне значения. И в итоге получил массив эмодзи в нужной мне модели:

MCEmoji(
    // Массив шестнадцетиричных ключей.
    emojiKeys: [0x1F600],
    // Флаг, который отражает, доступны ли у этого смайлика разные оттенки кожи.
    isSkinToneSupport: false,
    // Версия эмодзи.
    version: 1.0
)

Дальше отсортировал их по категориям и после этого упорядоченный список, который я так долго искал, был готов ????

Обратная совместимость

После того, как был готов упорядоченный список с указанными Unicode версиями для каждого эмодзи, нужно было соотнести версии Unicode и iOS.

Тут нет какого-то супер нативного способа, сделал это спустя несколько часов изучения сайта emojipedia.org. Там есть подобного формата новости: “In March 2022 iOS 15.4 included brand new emojis from Emoji 14.0” по которым получилось все соотнести.

Вот как это выглядит в коде:

private let maxCurrentAvailableEmojiVersion: Double = {
	let currentIOSVersion = (UIDevice.current.systemVersion as NSString).floatValue
	switch currentIOSVersion {
	case 12.1...13.1:
		return 11.0
	case 13.2...14.1:
		return 12.0
	case 14.2...14.4:
		return 13.0
	case 14.5...15.3:
		return 13.1
	case 15.4...:
		return 14.0
	default:
		return 5.0
	}
}()

Теперь для того, чтобы нужные эмодзи отображались для нужной версии iOS, мне осталось проверить, что версия эмодзи меньше, либо равна maxCurrentAvailableEmojiVersion.

Выбор тона кожи

Перед началом реализации логики выбора тона кожи, первой мыслью было хранить массив эмодзи со всеми видами тона кожи для каждого (P.S. Так многие и делают. Ну, а как по другому?).

После очередного просмотра списка эмодзи, я увидел одну закономерность. Она заключалась в том, что если смайлик поддерживает выбор тона кожи, то все элементы массива остаются неизменными, но на второе место в массиве вставляется определенное значение для каждого тона кожи.

И эта закономерность повторяется для каждого смайлика, у которого есть возможность выбрать тон кожи.

Оказалось, что действительно внизу этого файла есть раздел “Components”, где лежит список шестнадцатеричных значений для каждого тона кожи:

Это спасло меня от большого дублирования смайликов с разными типами кожи. Ведь теперь можно подставлять на второе место в массив значений эмодзи - значение нужного тона кожи и в итоге получу желаемый смайлик. А хранить в модели только изначальный массив значений ????

После выбора тона кожи, MCEmojiSkinTone.rawValue сохраняется в UserDefaults для нужного смайлика, который и является ключем для записи.

После этого, можно обновить модель и получить эмодзи с выбранным тоном кожи.

Двусоставные смайлики

Однако, есть еще и «двусоставные» эмодзи.

Элемент выбора тона кожи для «двусоставных» смайликов в iOS
Элемент выбора тона кожи для «двусоставных» смайликов в iOS

На фото видно, что для левой и для правой стороны эмодзи, в верхней части этого элемента - есть своя строка с тонами кожи. И на каждой строке противоположная сторона эмодзи закрашена сплошным цветом.

К сожалению, тона кожи, который закрашивал бы сплошным цветом смайлик в том файле не было ????

Я подозреваю, что для упрощения существует подобный код, который можно подставить и эмодзи станет закрашенным. Но пока я его не нашел. Поэтому не смог сделать этот элемент.

P.S. Если вы знаете решение этой проблемы, пожалуйста, свяжитесь со мной ???? Очень хочу сделать этот элемент тоже.

UI. Что тут могло пойти не так?

Иконки для категорий

Так как хотелось сделать элемент идентичным, иконки для категорий тоже должны были быть такими же.

Оригинальные иконки категорий из MacOS
Оригинальные иконки категорий из MacOS

Оказалось, найти их не так уж и просто. Они не лежат по первому запросу в гугле и даже если мне как-то удавалось их найти, они были в плохом качестве.

После долгих поисков, по всевозможным ресурсам, мне удалось их найти в системном, приватном фреймворке MacOS. Путь к желаемой папке выглядит так: /System/Library/PrivateFrameworks/CharacterPicker.framework/Versions/A/Resources ???? Они лежат там в отличном качестве, в формате pdf.

Но я не хотел тянуть в библиотку изображения и хотел попрактиковаться в отрисовке кодом. В этом мне очень помогла программа PaintCode. Так получилось убить сразу двух зайцев: уменьшить количество файлов библиотеки и попрактиковаться с UIBezierPath.

После практики с отрисовкой категорий, верстка элемента превью и выбора тона кожи, уже казалась вполне реальной задачей.

Элемент превью и выбора тона кожи

Элемент выбора тона кожи для эмодзи в MacOS ожидаемо выглядит не в стиле iOS.

Элемент выбора тона кожи в MacOS
Элемент выбора тона кожи в MacOS

Поэтому решил взять этот элемент, а за одно и элемент превью смайлика из стандартной клавиатуры на iPhone.

Элементы выбора тона кожи и превью из iOS
Элементы выбора тона кожи и превью из iOS

С элементом превью все было легко: фиксированные размеры и фиксированная позиция по центру нажатой ячейки.

Но вот с элементом выбора тона кожи все было чуть сложнее. Так как уже нужно было учесть:

  • Расположение выбранного эмодзи по X(исходя из этого значения выбирается направление верхнего прямоугольника с типами кожи).

  • Так же нужно было отследить, чтобы верхний прямоугольник не выходил за края самого пикера.

  • Правильно обработать все жесты.

Код верстки и расчетов получился слишком большой для того, чтобы его вставить в статью, поэтому оставлю ссылки: элемент выбора тона кожиэлемент превью.

Там осталась одна нерешенная проблема. В реализации выбора тона кожи от Apple можно начать выбор сразу, как только этот элемент появляется, не отрывая палец от экрана. Двигая палец вправо или влево будет подсвечиваться тон кожи, который совпадает по X с расположением пальца на экране.

Я повторил этот функционал, но у меня не получилось сделать так, чтобы можно было не отрывая палец после нажатия, сразу обработать событие перемещения по экрану. Ни один из жестов в моем случае так и не заработал, одновременно с жестами нажатия и логикой презентации этого элемента.

Вы можете попробовать исправить это в репозитории проекта и отправить pull-request. Буду очень рад, если у вас получится!

Локализация

С локализацией тоже было все непросто. Если локализацию ключей для поиска смайликов я с трудом, но нашел, то локализованного списка названий, для категорий эмодзи, у меня никак не получилось найти.

Локализовывать через переводчик не хотел, так как в таком случае 100% были бы ошибки.

Решение было очень топорным, но 100% давало мне верный перевод. Я вручную несколько часов сидел, переключал язык на телефоне, делал скриншот названий всех 9 категорий. Благо iPhone научился распознавать текст на фото и можно было копировать названия с полученных изображений(но все же не все языки так просто поддавались и приходилось иногда подключать Google Translate для определения корректного текста на фото).

В итоге, получилось собрать локализацию для всех стандартных языков в iOS.

На этом приключения не закончились. В библиотеке я хотел поддерживать сразу два менеджера зависимостей: Swift Package Manager и CocoaPods. Но локализация в них настраивается по разному.

В Swift Package Manager обращение к ресурсу локализации выглядит так:

NSLocalizedString(”a_localized_string”, bundle: Bundle.module, comment: “a comment”)

В качестве параметра bundle передается Bundle.module. Этот статический параметр автоматически добавляет сам SPM.

CocoaPods такого не умеет. Там для доступа к ресурсу локализации нужно сделать следующее:

let path = Bundle(for: LibraryName.self).path(
	forResource: "LibraryName",
	ofType: "bundle"
) ?? ""
NSLocalizedString(”a_localized_string”, bundle: Bundle(path: path) ?? Bundle.main, comment: “a comment”)

Эту проблему удалось решить с помощью расширения для Bundle, которое будет добавляться только если библиотека используется не через Swift Package Manager:

#if !SWIFT_PACKAGE
extension Bundle {
    static var module: Bundle {
        let path = Bundle(for: MCUnicodeManager.self).path(
            forResource: "MCEmojiPicker",
            ofType: "bundle"
        ) ?? ""
        return Bundle(path: path) ?? Bundle.main
    }
}
#endif

Это расширение позволяет всегда иметь доступ к Bundle.module в библиотеке, независимо от того, через какой менеджер зависимостей она используется.

Заключение

В итоге, у меня получилась библиотека, которая позволяет буквально в 3 строчки кода добавить элемент выбора эмодзи в ваше приложение.

Плюсом к этому она:

  • Поддерживает Swift Package Manager и CocoaPods.

  • Весит всего 795 килобайт.

  • И единственная из аналогов поддерживает все стандартные локализации(на момент публикации статьи, конечно ????).

Превью работы MCEmojiPicker
Превью работы MCEmojiPicker

Не ожидал, что такой простой, с первого взгляда, элемент станет таким интересным вызовом.

Дальше мне предстоит работа по оптимизации, так как оказалось, что отображать эмодзи в UILabel - затратная задача для оперативной памяти.

А так же, из не реализованных стандартных функций осталось добавить: поиск и раздел недавно использованных смайликов. Их рассчитываю доделать уже в ближайшее время.

Посмотреть код и поставить ⭐️, если понравилась статья можно тут:

Спасибо за внимание. Буду рад комментариям и предложениям.

Комментарии (5)


  1. Gargo
    00.00.0000 00:00

    Не совсем понятно - а зачем вы проверяете версию iOS для смайликов? Вы скрываете те, которые в обычной ситуации выглядят как знаки вопроса?


    1. ivanizyumkin Автор
      00.00.0000 00:00

      Я скрываю смайлики которые не доступны для версии iOS пользователя. Так как у них нет обратной совместимости.


  1. NeoCode
    00.00.0000 00:00
    +1

    Был бы весьма интересен подобный элемент в реализации для Qt (кроссплатформенный для десктопных приложений). И с возможностью выбирать не только эмодзи, но и любые unicode символы. Наверное и написать такое не очень сложно, но времени нет, а вдруг есть готовое решение где нибудь на гитхабе или в какой-нибудь коллекции виджетов?


  1. debug45
    00.00.0000 00:00
    +1

    Не тормозит ли такой список на слабом устройстве при быстром скролле? В своё время делал похожую штуку для Windows Phone, и приходилось сильно париться над оптимизацией рендеринга подобного пикера.


    1. ivanizyumkin Автор
      00.00.0000 00:00
      +1

      Не, не тормозит но ест много оперативки. Это вообще проблема в iOS, отображать эмодзи в UILabel тяжелая задача. Особенно когда их так много. Буду искать решение этого в ближайшие время