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

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




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

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


О спикере: Антон Сергеев (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.

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



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

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

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

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



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

Попробуем совместить все типы ограничений, с которыми мы познакомились, в одну систему. Допустим, мы хотим решить задачу не на всей прямой, а лишь между 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 — это прямоугольники, но проблема в том, что внутри Казуара эта информация не содержится, ее нужно туда дополнительно внедрить. У нас есть механизм внедрения дополнительных ограничений. Если введем для каждой view набор ограничений с положительными шириной и высотой, то на выходе всегда будем получать прямоугольники. Именно поэтому мы не можем сверстать с помощью Auto Layout view с отрицательными размерами.

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



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



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

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

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

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

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

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

Важно знать:

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

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

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


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

Оптимизация


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

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

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


Задача такая: создаем цепочку view одну за одной, и каждую последующую связываем с предыдущей. Таким образом выстраивалась последовательность из 1000 элементов. После замерили различные операции, время указано в миллисекундах. Значения достаточно велики, потому что 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 2019. Напомню, мы перенесли AppsConf с осени на весну, и следующая самая полезная конференция для мобильных разработчиков пройдет 22 и 23 апреля. Пора-пора задуматься о теме для выступления и подать доклад, или обсудить с руководителем важность походов на конференцию и забронировать билет.

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


  1. andy_p
    30.01.2019 17:30

    Было бы интересно сравнить с тем, как верстает TeX.


  1. uninova
    30.01.2019 19:30

    Спасибо за статью, очень интересно.

    Хотел бы добавить, что изменение приоритета ограничения(constraint) с optional (<1000) на required (=1000) или наоборот с required на optional, приведет к ошибке в райнтейме. Интересно было бы узнать, почему Auto Layout так себя ведет, что ему мешает сделать просто перерасчет.


  1. andreyverbin
    31.01.2019 00:13
    +1

    Стоит отметить, что далеко не все получится описать линейными уравнениями и, равносильно, ограничениями. Самый удобный layout который я видел это тупейший anchor layout в древнем Delphi образца 1998 года. Далеко с ним не уедешь, зато знаешь чего ждать и когда пора писать код руками. С сложными системами верста постоянно себя чувствую программистом на XML, когда ясную задачу прописываешь на максимально неподходящем для неё языке. В конце концов запили построитель прямоугольников и пишу все в коде, без линейной алгебры и сюрпризов.


  1. nubideus
    31.01.2019 02:40

    Как раз таки Autolayout в чистом виде очень предсказуемо себя ведет. Проблемы у новичков возникают, когда нужно взаимодействие легаси компонентов с ним, например UITextView в self-size ячейке, который должен растягиваться под размер введённого текста.


    1. andreyverbin
      31.01.2019 02:49
      +1

      Не могу согласиться, даже в чистом виде сложный layout быстро теряет предсказуемость и это свойство всех layout систем. Короче я ниасилил, сдался и сделал свой велосипед :) Последней каплей для меня стало прочтение мануала о том, как делать layout внутри scroll view.


  1. vintage
    31.01.2019 06:50
    +1

    То есть вместо простого, надёжного и предсказуемого flex-layout они накрутили гору матана, который работает на порядки медленнее, а конфигурировать и дебажить который на порядки сложнее.


    1. Zenitchik
      31.01.2019 14:54

      Ой, да ладно! «Линейка» — это ни разу не матан.


    1. Dim0v
      31.01.2019 15:16

      вместо простого, надёжного и предсказуемого flex-layout

      Если я правильно понял, о чем вы, то все (или почти все), что умеет flex-layout — умеет один единственный UIStackView и предоставляет очень похожие способы управления этим всем. Точно так-же "просто, надежно и предсказуемо". Autolayout же в общем случае предоставляет намного больше гибкости и возможностей. И не сказал бы, что он сильно сложен в использовании или "непредсказуем".
      Медленнее работает и дебажить иногда сложнее — это да. Но проблемы с производительностью, как правило, бывают только в сильно сложных UITableView/UICollectionView с автосайзингом ячеек и прочими радостями. А дебаг с визуальным инспектором сейчас в некоторых случаях даже проще, чем ползание по коду в поисках, кто же там фрейм портит.


      1. vintage
        31.01.2019 23:26

        flex ещё во wrap умеет. Так какой макет позволяет сверстать Autolayout, который не сможет flex?