Главная » Хабрахабр » Математические основы Auto Layout

Математические основы Auto Layout

Многие разработчики считают, что Auto Layout — это тормозная и проблемная штука, и крайне сложно заниматься его отладкой. И хорошо, если этот вывод сделан на основе собственного опыта, а то бывает и просто «я слышал, не буду даже и пытаться с ним подружиться».

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

Прежде, чем углубиться в неё, поговорим о современной верстке вообще.
Auto Layout — это система верстки. Рассмотрим особенности в имплементации Auto Layout в iOS, и попробуем выработать практические советы, которые могут помочь в работе с ним. Затем займемся Auto Layout — разберемся какую задачу он решает и как это делает.

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

О спикере: Антон Сергеев (antonsergeev88) работает в команде Яндекс.Карт, занимается мобильным клиентом для Карт на iOS. До мобильной разработки занимался системами управления электростанциями, где цена ошибок в коде слишком высока, чтобы их допускать.

Обозначения

Системы линейных уравнений знакомы нам еще со школы — обозначаются фигурной скобкой, а их решение — уже без. Также у систем линейных уравнений есть сущности, которыми оперирует Auto Layout — ограничения. Обозначаются прямой линией.

В честь казуара (лат. Странная и, как мы уже знаем, опасная птица не случайно нарисована в верхнем углу слайда. Cassowary), который, конечно, обитает в Австралии, во всех наших Айфонах назван алгоритм.

В Auto Layout есть свои ограничения, будем обозначать их цветами по порядку приоритета: красный — required; желтый — high; синий — low.

Верстка

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

Знать эти четыре значения достаточно, чтобы представить любую View.

Алгоритм № 1

Пока располагали казуара на листе, мы ненавязчиво описали первый алгоритм верстки:

  • определяем координаты и размеры;
  • применяем их к UIView.

Алгоритм работает, но достаточно сложен в применении, поэтому дальше будем его упрощать.

Предположим, что ниже — решение некоторой системы линейных уравнения.

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

Это подводит нас к интересной и довольно тонкой идее, с которой начинается вся современная верстка. Прелесть линейных преобразований в том, что они обратимы.

Мы хотим расположить ее так, чтобы центр совпадал с заданной точкой. Пусть есть View — прямоугольник со своими координатами и размером. Центр мы смоделируем с помощью линейных преобразований — координата левого верхнего угла + половина ширины.

Мы смоделировали центр линейным преобразованием, его не было: были только координаты левой верхней точки, ширина и высота.

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

Именно идея линейных преобразований позволяет нам создавать различные системы верстки.

Рассмотрим на элементарном примере. Выпишем систему, с помощью которой установим координаты середины и правой стороны, ширину и соотношение между шириной и высотой. Решим систему и получим ответ.

Так мы подошли ко второму алгоритму.

Алгоритм № 2

Вторая итерация алгоритма состоит из таких пунктов:

  • составляем систему линейных уравнений;
  • решаем ее;
  • применяем решение к UIView.

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

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

Выходов из этой ситуации не так уж и много:

  • Можно упасть — это очень распространенный метод. Кто работает с MacOS, знает, что NSLayoutConstraintManager так и поступает.
  • Возвращать значение по умолчанию. В контексте вёрстки, мы всегда можем вернуть все нули.
  • Более известный и деликатный способ — не допускать некорректный ввод. Этим способом пользуются популярные системы верстки, например, Yoga, известная под названием Flex Layout. Такие системы стараются создать интерфейс, который не допустит некорректный ввод.
  • Существует еще один способ в решении абсолютно всех задач — это переосмыслить все с самого начала и изначально не допустить возникновения этой проблемы. Auto Layout пошел именно этим путем.

Auto Layout. Постановка и решение задачи

У нас есть прямоугольная картинка и чтобы однозначно ее определить, нам необходимы 4 параметра:

  • координаты левого верхнего угла;
  • ширина и высота.

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

Одного значения: X = XP достаточно, чтобы определить положение точки. Все очень просто: пространство — это прямая, а все объекты, которые в нем могут разместиться — точки на прямой.

Есть пространство, в котором задаются ограничения. Рассмотрим подход Auto Layout. Решение, которое мы хотим получить — это X = X0, и никакое другое.

Мы не можем напрямую сделать вывод из записи, что X = X0, не можем ни на что умножить и ни с чем сложить. Есть проблема — у нас не определены операции с ограничениями. Для этого нам нужно преобразовать ограничение в то, с чем мы умеем работать — в систему уравнений и неравенств.

Auto Layout преобразует систему уравнений и неравенств следующим образом.

  • Сначала вводит 2 дополнительных переменных, которые не отрицательны и зависят друг от друга. Хотя бы одна из них равна нулю.
  • Само ограничение преобразуется в запись X = X0 + a+ — a-.

Точка X0 — решение системы: если a+ и a- будут равны нулю, то это будет верно. Но и любая другая точка на этой прямой будет решением.

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

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

Ограничения в виде неравенств

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

На графике выше видно, почему это так — любое значение a+ при a- = 0 (от X0 до +∞) будет оптимальным решением для задачи.

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

Так как мы хотим, чтобы все эти ограничения выполнялись одновременно, то функционал будет равен сумме всех функционалов от каждого ограничения. Для каждого ограничения дополнительно вводится по паре переменных, и составляется функционал.

Как мы и ожидали, составляя ограничения. Собираем функцию f и видим, что решение — это X1. Так мы подошли к третьему алгоритму.

Алгоритм № 3

Чтобы что-то сверстать, нужно:

  • составить систему линейных ограничений;
  • преобразовать ее в задачу линейного программирования;
  • решить задачу любым известным способом, например, симплекс-методом, который используется в Auto Layout;
  • применить решение к UIView.

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

Какое решение мы ожидаем увидеть?

  • X1? Ведь в первом ограничении так и написано: X = X1, и это решение конфликтует со вторым ограничением.
  • X2? Будет конфликт уже с первым ограничением.

Для выхода из ситуации мы выполним преобразования, которые уже делать умеем.

Это называется неопределенность. График нового функционала выглядит по-другому: любая точка из промежутка от X1 до X2 будет корректным валидным решением системы.

Неопределенность

У Auto Layout есть механизм для решения таких задач — приоритеты. Напоминаю, что желтым будем обозначать высокий приоритет, а синим — низкий.

Обратите внимание, что полученная система просто черного цвета. Преобразуем ограничения. Она есть в функционалах, которых здесь будет целых два. Мы умеем с ней работать, и в ней нет никакой информации об ограничениях. Auto Layout сначала будет минимизировать первый, а затем второй.

Разумеется, мы хотим, чтобы эта область составляла лишь одну точку, и Auto Layout действует таким же способом. В задачах линейного программирования мы ищем не само решение, а область допустимых решений. Вторую задачу линейного программирования Auto Layout решает уже на полученной области допустимых значений. Сначала он минимизирует самый приоритетный функционал на (- ∞, +∞) и на выходе получает область допустимых решений. Такой механизм называется иерархией ограничений, и дает в этой задаче точку X2.

Алгоритм № 4

  • Составить иерархию линейных ограничений;
  • преобразовать её в задачу линейного программирования;
  • решить последовательно задачу линейного программирования — от наиболее приоритетной к наименее приоритетной.
  • применить решение к UlView.

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

Здесь есть серьезная проблема — бесконечность, и я не знаю, что это такое.

Именно для этого были придуманы несколько типов ограничений: Алгоритм Cassowary под капотом Auto Layout не был уже существующим механизмом, который удобно лег на задачу Auto Layout, а придумывался как инструмент верстки, и в нем были предусмотрены специальные механизмы, чтобы уходить от бесконечностей еще в самом начале.

  • Параметры — это те ограничения, с которыми мы работали. В оригинале они называются preferences, иногда в документации Apple — optional constraints.
  • Требования или requirements — ограничения с приоритетом required.

Посмотрим, как требования с такими приоритетами преобразовываются с точки зрения математики.

На слайде оно красное, то есть это ограничение с приоритетом required — будем называть его требованием. У нас опять прямая с двумя точками, и первое ограничение — X = X1.

Больше ничего нет — никаких задач линейного программирования, никаких оптимизаций. Auto Layout преобразует его в систему линейных уравнений, содержащую одно уравнение X = X1.

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

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

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

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

Относительно прошлой системы добавились две дополнительные переменные — c и d, но в функционалы они не попадут, так как ограничения типа required никак не влияют на функционал в его первоначальном виде.

Кажется, что задача почти не изменилась — минимизируем то же самое, что и раньше, но меняется исходная область допустимых значений, теперь она от X0 до X3.

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

С этим нужно быть очень аккуратным, потому что излишнее злоупотребление required constraints приведет к задаче без решений, и Auto Layout с ней не справится.

Мы приходим к последнему пятому алгоритму.

Алгоритм № 5

  • Определить необходимые ограничения — требования к верстке;
  • составить иерархию линейных ограничений;
  • преобразовать все ограничения в задачу линейного программирования;
  • решить задачу линейного программирования;
  • применить решение к UlView.

Мы рассмотрели Cassowary — алгоритм, который находится внутри Auto Layout, но при его реализации возникают различные особенности.

Особенности в iOS

В layoutSubviews() нет расчетов.

Ответ: всегда, в любой момент времени Auto Layout посчитан. Когда же они производятся? Расчет происходит ровно тогда, когда мы добавляем constraints на нашу view, либо активируем их с помощью современных методов работы API с constraints.

У нас есть механизм внедрения дополнительных ограничений. Наши view — это прямоугольники, но проблема в том, что внутри Казуара эта информация не содержится, ее нужно туда дополнительно внедрить. Именно поэтому мы не можем сверстать с помощью Auto Layout view с отрицательными размерами. Если введем для каждой view набор ограничений с положительными шириной и высотой, то на выходе всегда будем получать прямоугольники.

Вторая особенность — это intrinsicContentSize — свойственный размер, который можно задать каждой view.

Этот механизм очень удобен, он позволяет уменьшать количество явных ограничений, что упрощает использование Auto Layout. Это простой интерфейс для создания 4 дополнительных ограничений-неравенств, которые будут помещены в систему. Последний и самый тонкий момент, про который часто забывают — это TranslateAutoresizingMaskIntoConstraints.

Это костыль, который ввели еще во времена iOS 5, чтобы старый код после появления Auto Layout не поломался.

Внутри view мы используем view, которая про constraints ничего не знает, верстает все на фреймах, но внутри себя она верстает view, которую уже давно перевели на constraints. Представьте ситуацию: мы верстаем view на constraints.

Напоминаю, внутрь задачи Auto Layout Казуара никакие фреймы не приходят, только ограничения.

Размер и положение view, которая была сверстана на фреймах, полностью не определяются через constraints. При расчете размера и положения всех других view будут учитываться некорректные размеры, даже несмотря на то, что после Auto Layout мы применим туда корректные фреймы.

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

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

Важно знать:

  • Если мы создаем view из Interface Builder, то значение по умолчанию для этого свойства будет false.
  • Если же мы создаем view непосредственно из кода, то оно будет true.

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

Практические советы

Всего совета будет три и начнем с самого важного.

Оптимизация

Важно локализовать проблему.

Вы когда-нибудь сталкивались с проблемой оптимизации экрана, который сверстан на Auto Layout? Скорее всего нет, чаще вы сталкивались с проблемой оптимизации верстки ячеек внутри таблицы или Сollection View.

Чтобы ее локализовать и оптимизировать, посмотрим на эксперимент. Auto Layout достаточно оптимизирован, чтобы сверстать любой экран и любой интерфейс, но сверстать сразу 50 или 100 для него проблема. Цифры взяты из статьи , где Казуар впервые был описан.

Таким образом выстраивалась последовательность из 1000 элементов. Задача такая: создаем цепочку view одну за одной, и каждую последующую связываем с предыдущей. Значения достаточно велики, потому что Auto Layout был придуман еще на стыке 80-х и 90-х годов. После замерили различные операции, время указано в миллисекундах.

Собирая такую цепочку, можно действовать следующим образом:

  • Последовательно добавлять по одному constraint и каждый раз решать. При этом будет затрачено 38 секунд.
  • Можно добавить сразу все ограничения единовременно, и только потом решать систему. Это решение эффективнее. По старым данным эффективность возрастает на 70%, но в текущей реализации на современных устройствах будет всего 20%. Но качественно единовременное добавление ограничений всегда будет эффективнее.
  • Когда вся цепочка собрана, можно добавить еще одно ограничение. Как видно из таблицы, это операция достаточно дешевая.
  • Самое интересное: если мы не добавляем никакие новые ограничения, а изменяем какую-то константу в одном из существующих — это на порядок эффективнее, чем удалять или создавать новое ограничение.

Первые два пункта можно описать как первичный расчет интерфейсов, два последних — как последующий.

Первичный расчет интерфейса

Здесь можно воспользоваться методами массового добавления constraints для оптимизации:

  • NSLayoutConstraints.activate(_:) — при создании view собирать все constraints последовательно в массив, кэшировать и только потом единовременно добавить.
  • Либо создавать ячейки в Interface Builder. Он все сделает за нас, и проведет дополнительную оптимизацию, что часто бывает удобно.

Последующие расчеты интерфейса

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

  • Скрывать UIView — самый интересный и малоиспользуемый прием. При удалении view чистится весь кэш, который был сохранен в Auto Layout. Если мы ее просто скроем, то кэш не сотрется, но при этом можно сверстать view, которая будет иметь другое отображение.
  • Управлять приоритетами свойственных ограничений — IntrinsicContentSize. Эффективный метод, который позволяет хорошо справляться с ячейками, но про него часто забывают.
  • Создавать больше типов ячеек. Если ваши ячейки очень сильно различаются одна от другой, возможно, они разных типов.

Чтобы познакомиться подробно с приемами, советую посмотреть сессию WWDC 2018S220 High Performance Auto Layout. Она уникальна — Apple глубоко забирается в реализацию и описывает много удобных механизмов, которые позволяют создавать ячейки оптимально.

Проектирование ограничений

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

Начните с приоритетов

Чем больше required ограничений, требований — тем больше вероятность, что вы однажды придете к противоречивой системе, которая не будет иметь решения. У вас возникнут проблемы.

В любой непонятной ситуации понижайте приоритет, что бы ни случилось.

Здесь действуют очень простые правила:

  • Чем меньше компоненты, тем больше приоритеты. Чем меньше вы верстаете компонент (кнопку или loader) — тем выше должен быть приоритет.
  • Чем больше компоненты, тем меньше приоритеты. Если вы делаете огромный экран, то приоритеты должны быть низкие.

Замораживайте требования

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

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

Это напрямую исходит из того, что задачи в иерархии приоритетов решаются последовательно — от более приоритетных к низкоприоритетным. Неочевидный вывод — чем ниже приоритет, тем дешевле модификация ограничения. Auto Layout это понимает и решает систему оптимально, если вы меняете только низкоприоритетные ограничения. Если мы что-то меняем в низком приоритете, то на решение верхних это никак не отразится, и область допустимых значений не изменится.

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

Используйте неравенства

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

Приемы достаточно простые: Плюсы неравенства, типа required, в том, что с ними гораздо сложнее создать противоречия.

  • Чем выше приоритет, тем больше должно быть неравенств.
  • И наоборот, чем меньше приоритет, тем больше можно использовать равенств.

Самое главное, что я хотел донести в этой статье, это понимание того, как мы приходим к ограничениям.

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

Требования и параметры — это принципиально разные вещи. Мы привыкли работать с ограничениями так, как будто они бывают просто разных приоритетов, но важно понимать и помнить, что принципиально — и с точки зрения математики, и внутри при решении — ограничения типа required и все остальные решаются принципиально по-разному.

Полезные ссылки:

Solving Linear Arithmetic Constraints for User Interface Applications
The Cassowary Linear Constraint Solving Algorithm
Constraints as s Design Pattern
Auto Layout Guide by Apple
WWDC 2018 Session 220 High Performance Auto Layout
Магия UILabel или приватное API Auto Layout — Александр Горемыкин
Блог Яндекс.Карт на Medium

Напомню, мы перенесли AppsConf с осени на весну, и следующая самая полезная конференция для мобильных разработчиков пройдет 22 и 23 апреля. Кстати, мы уже приняли доклад Антона в программу AppsConf 2019. Пора-пора задуматься о теме для выступления и подать доклад, или обсудить с руководителем важность походов на конференцию и забронировать билет.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

День Радио: патенты Маркони и Попова

Не имея большого желания присоединяться к более чем столетнему холивару Попов vs. Маркони, все же воспользуюсь грядущим очередным Днем Радио (который традиционно отмечается в нашей стране 7 мая), чтобы на примерах А.С. Попова и Г. Маркони напомнить почтеннейшей публике о ...

Обзор цифровой ручки MT6081 — ваши заметки сразу на компьютере

Вот чего, конечно, у «Даджета» не отнять, так это умения называть свои гаджеты странными символами: куда ни глянь, то MT1104, MT4017, MT… и так далее. Мы добрались до модели MT6081 — это довольно любопытная смарт-ручка, и мы вам расскажем, чем ...