Типографика в iOS

    Большинство информации в приложениях передается посредством текста. Поэтому верстать его приходится много, а незнание всей механики рендеринга влечет за собой различные проблемы. Например, простая задача — добавить выделение текста в существующее приложение. Заменяем UILabel на UITextView, и вдруг едут все отступы, текст выглядит совершено по-другому или вообще не влезает на экран.



    Под катом вы найдете расшифровку выступления Ирины Дягилевой на AppsConf, в котором она объяснила, почему это происходит и какие настройки лучше использовать в том или ином случае.

    Статья будет состоять из двух частей, сначала мы поговорим про основные термины типографики, про шрифты и их метрики и про наиболее часто используемые символьные атрибуты. А во второй части мы подробно поговорим про TextKit и отличия рендеринга UITextView и UILabel.

    О спикере: Ирина Дягилева ведущий iOS разработчик в компании RAMBLER&Co. За многолетний опыт iOS разработки успела поучаствовать в создании нескольких приложений для крупных газетных издательств, в которых нужно было осуществлять полный контроль над отрисовкой текста.



    Термины типографики


    Сначала разберемся с основными терминами типографики, поговорим про шрифты и их метрики, и про наиболее часто используемые символьные атрибуты.

    Шрифт


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


    Шрифты в пределах одной группы имеют различные начертания. Начертание, в свою очередь, тоже делится на три характеристики. Мы различаем символы по ширине, по насыщенности и по наклону. Часто происходит путаница между курсивным и наклонным начертанием. Это совершенно разные начертания, курсивное изначально содержится в файле шрифта, и оно лишь схоже с нормальным. А наклонное это программно-синтезированное искажение символов из прямого начертания. И наконец третья составляющая — это размер или по-другому кегль.


    Каждому символу на печатной странице отводится определенное место, этот прямоугольник называется кегельной площадкой. Его высота от нижнего до верхнего края называется кегль или размер шрифта. Система умеет возвращать нам прямоугольник, который отводится символу на печатной странице, но на самом деле его высота не равна кеглю. Он называется Bounding Rect и состоит из следующих величин: межстрочный зазор (leading) и высота строки. Высота строки в общем случае тоже не равна кеглю.


    Если мы посмотрим на все символы, то они как будто лежат на одной прямой. Эта линия называется линией шрифта или Baseline, параллельно ей проходит средняя линия шрифта или Meanline. А расстояние между ними называется x-height, которое характеризует высоту строчных букв. За эталон взят символ х, отсюда и название метрики x-height. Также мы можем получить высоту заглавных букв — cap-height.


    И последние две метрики это ascender и descender. Всё то, что выходит выше Baseline до наивысшей точки верхних выносных элементов — это ascender, а descender — то, что уходит вниз, соответственно он отрицательный.


    Интерлиньяж


    Теперь давайте рассмотрим пару строк. Расстояние от базовой линии одной строки до базовой линии другой строки называется интерлиньяж или лидинг. Любимый дизайнерами параметр, который мы можем редактировать несколькими способами. Во-первых, мы можем изменить Line spacing. Он задается в абсолютных величинах и увеличивает расстояние лишь между строками. Отступ у первой строки остается неизменным.


    Следующая настройка Line Height Multiple задается в относительных величинах. Это некоторое значение, на которое домножится каждая строка текста. На рисунке ниже, мы видим, что у первой строки также добавился отступ. Понимать это, на самом деле, очень важно, потому что в результате таких изменений могут поехать другие отступы, например, у текстового элемента.


    Следующие два атрибута это Minimum Line Height и Maximum Line Height. Они задают ограничение строки. Например, для данного кегля и данного шрифта высота строки равна 200 поинтам. Если мы выставим Minimum Line Height = 170, то ничего не изменится, потому что мы удовлетворяем этим условиям. Но как только мы превысим значение в 200 пунктов, то мы увидим, что строки начали увеличиваться. И также обратите внимание на рисунок ниже, у первой строки появляется некоторый отступ. Аналогично и Maximum Line Height, увеличиваем значение — ничего не меняется, т.к. удовлетворяем условиям. Только начинаем уменьшать — строки начинают сближаться.


    Чтобы выбрать какую-то из этих настроек, необходимо сесть вместе с дизайнером и посмотреть, что именно он задал в своем графическом редакторе, и подобрать соответствующие атрибуты. Иначе потом будут проблемы с выравниванием текстовых элементов.

    И последний атрибут Baseline offset, который также зрительно отдалит строки друг от друга, но предназначен немного для другого, а именно для верхних и нижних индексов, например, х 2, логарифм по основанию. Уменьшаем размер шрифта и сдвигаем его на какое-то определенное значение относительно Baseline, используя этот атрибут.

    Абзац


    Теперь давайте подвигаем целые параграфы. Здесь на самом деле все просто, никаких сюрпризов, можем увеличивать значения после параграфа — Paragraph Spacing, можем задать Paragraph Spacing Before, и тогда перед параграфом добавится какой-то отступ. И заметьте, что первый параграф остался на своем месте, т.к. перед ним параграфа нет и соответственно перед ним ничего не добавилось.


    Мы можем сдвигать первую строку (First Line Head Indent) и также задавать отступ для всех остальных строк как слева (Head Indent), так и справа (Tail Indent). При этом, если Tail Indent задается отрицательным, отступ будет у правой границы, а положительный задает отступ от левой границы.

    Кернинг и Трекинг


    Если мы будем складывать символы друг за другом, то, в силу специфики самих символов, общая картина получится не очень. Например, на рисунке ниже символы «T» и «o» очень отдалены друг от друга, их хочется сблизить. А «r» и «n» наоборот слиплись, их хочется отдалить.


    Для этого придумали два понятия: кернинг и трекинг. Идут они всегда вместе, т.к. характеризуют одну величину — межсимвольные пробелы. Кернинг задается для конкретной пары символов и прописывается в файле шрифта, образуя целую таблицу кернинга. А трекинг задается для диапазона символов или всего документа, без привязки к конкретным символам. Т.е. если мы включим кернинг (по умолчанию он включен), то увидим, что символы стали отображаться по-другому. Мы можем как увеличивать, так и уменьшать это значение. В iOS мы можем задавать только трекинг и как-то модифицировать то значение, которое прописано в файле шрифта.

    Символы и глифы


    Те, кто когда-либо работал с текстом, уже знают, что есть методы, которые переводят диапазоны символов в диапазоны глифов и наоборот. Что это такое и почему их количество может не совпадать? Символ — это семантическая единица языка, это буква, цифра, математическая операция. А Glyph — это ее визуальное представление. И количество их может не совпадать при использовании лигатур. По умолчанию в iOS включены обязательные лигатуры, и мы видим, что идущие два подряд символа «l» превратились в один Glyph, было 5 символов, а стало 4 глифа. Также мы можем включить все лигатуры, которые прописаны в файле шрифта, и тогда мы получим еще больше красивых элементов, как на картинке ниже.


    TextKit


    Теперь давайте разбираться с фрэймворком TextKit, который появился в iOS 7 и предоставляет нам большие возможности в редактировании текста. Он состоит из определенного набора классов и протоколов для работы с текстом, основные его элементы: Layout Manager, Text Storage, Text View и Text Container. Чтобы понять за что каждый из них отвечает часто проводят аналогию с парадигмой MVC.


    Text Container и Text Storage — это Model или данные. Layout Manager — Controller. Text View — соответственно View. Теперь чуть подробнее о каждом из них.

    Text Storage — это хранилище. Оно содержит информацию о символах и их атрибутах, следит за тем, чтобы данные были консистенты, оповещает Layout Manager о том, что произошли какие-то изменения в данных.

    Text Container — это тоже модель, потому что он поставляет Layout Manager фрагменты строк, в которых необходимо отрисовать текст, помимо этого с его помощью задаются области исключения, которые текст будет обтекать.

    И наконец Layout Manager как и положено контролеру в MVC отвечает за очень много функций. Во-первых, он следит за Text Storage и Text Container. Генерирует глифы из символов, переводит диапазоны из одних в другие, и занимается непосредственной отрисовкой глифов.

    Инициализация


    Чтобы инициализировать Text Kit stack необходимо, во-первых, создать Text Storage и проинициализировать его какой-нибудь атрибутной строкой. После этого можно создать Layout Manager и добавить его в Text Storage. Причем, мы можем добавить столько Layout Manager, сколько нам хочется.


    Например, когда для одних и тех же данных существует несколько представлений. После этого создаем Text Container и добавляем его в Layout Manager. По аналогии мы можем добавлять его столько раз, сколько нам нужно, например, для многоколоночной верстки. И наконец последний опциональный шаг создание Text View, которому мы передаем Text Container, либо можем рисовать глифы напрямую через Layout Manager.

    UITextView и UILabel


    Все стандартные элементы UITextView и UILabel, которые отвечают за отрисовку текста, уже имеют в себе инициализированный Text Kit stack, но выглядят и рендерят текст совершено по-разному. На рисунке ниже никаких дополнительные настройки кроме шрифта не указаны, но мы видим, что текст выглядит совершено по-другому. Давайте разберемся почему. Во-первых, у UITextView текст начинается не от самого левого края. За это отвечает настройка lineFragmentPadding у Text Container, которая задает отступ у фрагмента строки слева и справа. Т.е. если мы ее выключим, то текст подвинется к самому краю.


    Также есть отступы сверху и снизу и по умолчанию они равны 8 поинтов. Если мы их выключим, то увидим, что текст поднялся наверх. И последнее, что очень бросается в глаза это межстрочный интервал или leading. Дело в том, что UITextView по умолчанию использует leading, заданный в фале шрифта, а UILabel — нет, отсюда и такая разница. В Layout Manager мы можем выключить эту настройку и получим точно такое же отображение, как и у UILabel.


    Многие не любят использовать Auto Layout и рассчитывают высоту текста фрэймами, чаще всего используя для этого метод boundingRectWithSize. Об этом методе есть очень много негативных отзывов, но на самом деле в него просто нужно передать правильные параметры. То есть, во-первых, нужно правильно передать Size, указать правильную ширину с учетом всех отступов. Если мы считаем высоту текста для Text View, то мы должны от ширины Text View не забыть вычесть отступы у фрагмента строки LineFragmentPadding, а также отнять отступы от Text Container. И только эту ширину уже передать в этот метод.

    Атрибутная строка не может знать, где она будет отображаться, в UILabel или UITextView. Поэтому ей надо передать какую-то дополнительную информацию, а именно какие-то опции, которые она будет использовать при подсчете высоты текста. Нас интересует две опции. UsesLineFragmentOrigin ее мы передаем, когда необходимо посчитать многострочный текст. И вторая настройка: нужно ли использовать при подсчете стандартный лидинг шрифта. Для UILabel мы не должны передавать эту настройку, а для UITextView, если в LayoutManager эта опция включена, должны не забыть передать. Тогда этот метод вернет правильную высоту.

    Отступы в UITextView


    Наверное, вы задались вопросом зачем нам целых две настройки: Line Fragment Padding, который задает отступы слева и справа, и Text Container Inset, которым тоже можно задавать отступы слева и справа. Зачем же Аpple придумал целых две настройки?


    Допустим мы с вами верстаем электронную книгу и хотим в центре расположить статичную картинку, которую текст будет обтекать. Задается это очень просто, у Text Container есть массив UIBezierPath областей исключения. Мы просто берем фрэйм этой картинки, переводим во фрэйм Text Container и отдаем Text Container, чтобы он эту область обтекал. Но на примере выше мы видим, что текст вплотную прилегает к картинке — это не очень красиво. Для этого нам как раз поможет Line Fragment Padding.


    На рисунке выше подсвечены фрагменты строк и Text View Background Color, чтобы было виднее. Если мы будем редактировать Text Container Inset, то отступ добавится по всему периметру текста, а если мы будем редактировать Line Fragment Padding, то у каждого фрагмента строки слева и справа добавится значение, которое мы указали. Т.е. текст выглядит совершено по-другому и не прилегает вплотную к картинке. Вот именно для этого Line Fragment Padding и придуман.

    Переносы


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


    Сначала необходимо определить язык, вставить возможные точки переносов, потом вставить дефис в нужное место, все это рассчитать. Сейчас мы можем задать HyphenationFactor у Layout Manager, и тогда переносы добавятся у всего текста. Либо можем задать у объекта NSParagraphStyle, тогда переносы добавятся для нужного параграфа. Значение HyphenationFactor варьируется от 0 до 1. Ноль означает, что переносов не будет вообще; единица, что их нужно добавлять всегда, когда текст полностью не заполняет строку; а промежуточное значение означает, что если текст заполнен меньше, чем на указанное количество процентов, то мы пытаемся вставить переносы.


    Мы уже рассмотрели, как задается обтекание в тексте. А, что, если нам нужно добавить картинку прямо в текст и отображаться эта картинка должна вместе с текстом. Делается это тоже очень просто, мы создаем NSTextAttachment, добавляем в него картинку, заворачиваем все это в атрибутную строку и работаем дальше как с обычным NSAttributedString. Но есть нюансы, мы не можем задать какой-то желаемый размер этой картинки, т.е. величина символов с attachment будет равна ширине и высоте самого изображения. И чтобы это как-то поменять или сдвинуть, необходимо наследоваться от NSTextAttachment, переопределить один метод и добавить какие-то свои отступы.

    Наследование


    Если мы хотим расширить функциональность, мы наследуемся от NSTextStorage, LayoutManager или TextContainer. Если с последними двумя все просто, то с NSTextStorage приходится немножко повозится. Потому что NSTextStorage на самом деле класс кластер. Т.е. создавая экземпляр NSTextStorage мы не знаем, экземпляр какого класса нам вернется. Это накладывает дополнительные ограничения на наследование. Во-первых, мы должны обеспечить свое хранилище данных, это может быть какая-то атрибутная строка, а также переопределить два метода по чтению и модификации символов и атрибутов в этой строке.


    Это может быть нужно, если мы с вами, например, делаем какой-то чатик и хотим напомнить всем докладчикам, что у них сегодня вечером состоится фуршет, а чтобы никто не пропустил, мы хотим отправить всему каналу уведомление. Мы пишем @channel и это автоматически подсвечивается.


    Чтобы это реализовать в NSTextStorage есть метод processEditing. Система вызывает его самостоятельно, когда мы оповестили LayoutManager, что произошли изменения. С помощью регулярного выражения нам нужно найти все вхождения определенных символов и добавить или наоборот удалить какие-то атрибуты.


    Чтобы добавить какую-то кастомную область отрисовки, тоже придется немного повозиться, просто из коробки этого сделать нельзя. Мы наследуемся от объекта NSTextContainer и, когда LayoutManager поставляет нам фрагменты строк, мы с помощью какого-то алгоритма начинаем определять области, в которых должен быть отрисован текст.


    На рисунке выше довольно сложная UIBezierPath, созданная из svg картинки. Перебором ищется пересечение исходного фрагмента строки с UIBezierPath. В LayoutManager возвращается только нужный кусочек, а оставшаяся часть строки записывается в RemainingRange, который придет на следующей итерации.

    Итак, Text Kit позволяет:

    • Добавлять динамической форматирование текста.
    • Задавать произвольные области отрисовки и обтекания.
    • Использовать TextAttachment.
    • Переносить слова по слогам.

    А также Text Kit помогает реализовывать некоторые другие интересные функции, такие как Dynamic type или анимация текста.

    А напоследок немного полезных ссылок:
    https://github.com/idva/typography-ios
    https://github.com/idva/text-attributes-ios
    Джеймс Феличи «Типографика. Шрифт, верстка, дизайн»
    Text Programming Guide for iOS
    Attributed String Programming Guide
    Getting to Know TextKit
    Butterick’s Practical Typography

    И контакты Ирины Дягилевой:
    https://www.linkedin.com/in/dyagileva
    https://github.com/idva

    Напоминаем, что конференция по мобильной разработке AppsConf в этом году вынесена из состава РИТ++ в отдельное мероприятие и пройдет 8 и 9 октября. Мы планируем организовать масштабное событие, собрать активистов всех российских сообществ мобильных разработчиков, представить более 60 докладов для более чем 500 человек.

    Конечно, мы ищем докладчиков, и будем рады не только признанным, широкоизвестным мастерам, но и новым лицам. Хотим подчеркнуть, что не стоит бояться подавать заявку, не имея большого опыта публичных выступлений — для того, чтобы доклад получился классным у нас есть школа докладчиков, в том числе в формате telegram-канала.

    До первого июня доступны early bird билеты по минимальной цене.
    • +31
    • 6,5k
    • 2
    Конференции Олега Бунина (Онтико) 640,14
    Конференции Олега Бунина
    Поделиться публикацией
    Комментарии 2
    • +2
      Очень полезно. Спасибо, что выкладываете тексты докладов.
      • 0
        Очень понравилась статья
        Огромное спасибо!

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

        Самое читаемое