Хабрахабр

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

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

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

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

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

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

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

Шрифт

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

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

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

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

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

Интерлиньяж

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

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

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

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

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

Абзац

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

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

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

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

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

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

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

Чтобы инициализировать 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 поинтов. Дело в том, что UITextView по умолчанию использует leading, заданный в фале шрифта, а UILabel — нет, отсюда и такая разница. И последнее, что очень бросается в глаза это межстрочный интервал или leading. В Layout Manager мы можем выключить эту настройку и получим точно такое же отображение, как и у UILabel.

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

Поэтому ей надо передать какую-то дополнительную информацию, а именно какие-то опции, которые она будет использовать при подсчете высоты текста. Атрибутная строка не может знать, где она будет отображаться, в 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 Container Inset, то отступ добавится по всему периметру текста, а если мы будем редактировать Line Fragment Padding, то у каждого фрагмента строки слева и справа добавится значение, которое мы указали. На рисунке выше подсвечены фрагменты строк и Text View Background Color, чтобы было виднее. текст выглядит совершено по-другому и не прилегает вплотную к картинке. Т.е. Вот именно для этого Line Fragment Padding и придуман.

Переносы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

До первого июня доступны early bird билеты по минимальной цене.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть