WWDC 2022 привнесла много изменений и улучшений в SwiftUI, и обновленный протокол Layout — одно из самых значимых. Об особенностях работы с новыми инструментами компоновки элементов, анонсированными в iOS 16.0, рассказывает iOS-разработчик студии CleverPumpkin Даниил Апальков. Прежде чем говорить о новых возможностях протокола, давайте посмотрим, как выглядит текущий механизм Layout’a в SwiftUI, и как с ним можно взаимодействовать сейчас. Принцип работы алгоритма простой: SwiftUI для каждого элемента в иерархии предлагает доступное место. Элемент на основе полученных данных решает, сколько места предложить дочерним внутри себя. После размещения элемент возвращает контейнеру размер, который ему необходим. Исходя из этой информации родитель располагает этот элемент внутри себя. На практике все чуть сложнее. В текущих версиях iOS мы взаимодействуем с механизмом Layout’a в основном через .frame модификатор. View в SwiftUI имеет несколько свойств и ограничений, определяющих необходимое ему место. Через .frame-модификатор можно указать максимальный, минимальный и идеальный размеры. Также у View есть предпочтения по отступам с разных сторон в различных контекстах, подробнее об этом написано в статье автора javier на swiftui-lab. При таком подходе в комбинации с .frame модификатором используются GeometryReader для прочтения предлагаемого контейнером размера и PreferenceKey для сохранения параметров одной или нескольких View, чтобы на их основании еще раз вернуться в корень иерархии и подкорректировать Layout. Такой подход абсолютно неочевиден, сильно усложняет код, а в случае с PreferenceKey при неправильном обращении может привести к бесконечному циклу Layout’ов и падению приложения. Давайте разберем новый Layout протокол, предложенный Apple, который позволяет внедриться непосредственно в описанный выше процесс. Весь протокол представляет собой небольшой набор методов и полей. Для начала сфокусируемся на двух основных: Можно отметить несколько особенностей: во-первых, протокол использует определенный тип для дочерних элементов - Subviews. Эта коллекция рандомного доступа предоставляет интерфейс для взаимодействия с элементами определенным путем, поэтому просто достать непосредственно View в процессе Layout’а у нас не выйдет. Обратите внимание, что ProposedViewSize — отдельный тип, который может быть инициализирован из привычного нам CGSize. Разница в том, что и предложенная ширина и высота в ProposedViewSize могут быть nil, размер может быть бесконечным или вовсе неуказанным, и таким образом мы можем позволить объекту занять свой идеальный размер.  Также у ProposedViewSize есть метод, который заменяет незаданные измерения исходя из значения размера, который передается как параметр метода. Здесь мы видим те самые магические 10 пойнтов отступа как значение по умолчанию: Давайте перейдем к практике и разберем работу протокола на примере простенького кругового Layout’а: Чтобы написать этот Layout, требуется небольшая подготовка: Нужно запросить размер. Для этого попросим в виде входных данных радиус окружности, и посчитаем самый большой элемент в коллекции, так как они могут быть разного размера. Для каждого из элементов мы будем опираться на его идеальный размер, то есть предложим нашим Subview размер шириной и высотой nil, чтобы они заняли необходимое пространство. Опираясь на количество Subview и радиус окружности, мы сможем посчитать координаты и выложить наши элементы по кругу внутри запрошенного места. Начнем с первого пункта: Обратите внимание: чтобы узнать размер у Subview, необходимо использовать метод sizeThatFits, предлагая ProposedViewSize. В нашем случае он указан как .unspecified, и Subview занимает столько места, сколько ему необходимо. В расчете мы используем “диаметр” плюс “высота”/“ширина” самого большого элемента в коллекции, чтобы все наши Subview поместились внутри. Теперь, когда мы попросили место, пора расположить элементы по кругу. Для этого высчитываем координату и размер, которые будем использовать, чтобы расположить Subview, используя метод place(at:proposal:): Готово! Теперь, когда мы увидели основной кейс использования протокола, давайте поговорим о других важных особенностях. Посмотрите на протокол ниже, и кратко пробежимся по моментам, которые мы еще не затронули: layoutProperties - информация о Layout’е, например о его направлении (горизонтальное или вертикальное); Cache - тип, который может быть использован для улучшения быстродействия Layout’a. Как вы могли заметить, мы несколько раз и в методе sizeThatFits и в методе placeSubviews вызывали метод getMaxSize. В некоторых случаях, между первыми двумя методами могут быть общие и дорогостоящие расчеты, которые как раз можно сохранить в Cache. Он является inout параметром и может быть модифицирован из любого места. В случае CircleLayout, я бы мог сделать Cache типа CGSize, и сохранить туда мой максимальный размер, чтобы не считать его дважды; makeCache - место для таких расчетов и сохранений. Также SwiftUI может по мере обновления экрана попросить обновить Cache, для чего и существует updateCache; Методы spacing, и explicitAlignment нужны для того, чтобы в зависимости от соответствующих свойств у Subviews определить, необходим ли spacing у нас как контейнера. И еще одна особенность, привнесенная iOS 16 в процесс Layout’а в SwiftUI - AnyLayout. С его помощью мы можем динамически менять Layout в зависимости от State с анимацией: Layout протокол - большой скачок для SwiftUI, решающий массу проблем. С его появлением в iOS 16 стало заметно проще решать задачи, связанные с выделением места и расположением элементов на экране с минимумом кода и максимумом читаемости. Но не стоит забывать, что этот новый инструмент пока на Бета-стадии, и до релиза он может добраться в несколько измененном виде.    Маркетолог в CleverPumpkin