Connect with us

Разработка

ArrangementView: подумайте, прежде чем компоновать

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

Опубликовано

/

     
     

Когда я впервые увидел API ArrangementView, в нём было что-то слегка неудобное, хотя я не сразу понял, что именно. Только когда я действительно попробовал его в Xcode 27.1 beta, это ощущение стало более конкретным. После того как я подробнее изучил материалы Apple и снова рассмотрел API в контексте iPhone Duo, я понял, что дискомфорт возникал по двум причинам. Во-первых, я просто не до конца понимал, для каких задач предназначен этот API; когда это стало ясно, часть вопросов исчезла. Но вторая причина была связана уже с самим API — и она осталась даже после того, как я понял его замысел.

Чтобы правильно использовать ArrangementView, полезно сделать ещё один шаг назад, прежде чем использовать его: как именно связаны эти два фрагмента содержимого? Какие решения можно доверить контейнеру, а какие компромиссы всё ещё должен определять сам продукт? Сначала подумайте — потом компонуйте.

Необычный контейнер

В отличие от обычных контейнеров компоновки, ArrangementView предназначен не просто для размещения произвольной пары представлений. Вместо этого он описывает отношение между двумя представлениями с помощью набора правил компоновки, а затем объединяет эти правила с доступным пространством, соотношением сторон, классами размеров и областями разделения устройства, чтобы определить, какое из представлений будет показано и где оно окажется. Apple называет такое отображение входных условий в итоговую компоновку arrangement (договоренность).

Чтобы пользоваться им правильно, нужно смотреть на API сразу с нескольких сторон:

  • Объявляйте отношения, а не геометрию. В этом смысле он немного похож на NavigationSplitView. Разработчик указывает, какое представление основное, а какое дополнительное, и выбирает split или overlay; фактическую геометрию контейнер определяет сам, исходя из окружения. Чтобы заранее понимать результат, нужно сначала разобраться в его правилах компоновки и fallback-поведении.
  • Видимость должна быть частью проектирования содержимого. В режиме split, если контейнер не может разделить доступное пространство по указанной оси, он может оставить только основное представление. Это уже не просто компромисс компоновки, а компромисс в информации и взаимодействии. Дополнительное представление может сохранить своё состояние и восстановить его при повторном появлении, но разработчику всё равно нужно продумать, как пользователь сможет получить доступ к содержащейся в нём информации, пока оно скрыто.
  • Конфигурация задаёт предпочтения, а не окончательный результат. splitArrangementLayoutRatio и splitArrangementFixedLayoutSize выражают предпочтения или ограничения размера, а overlayArrangementEdge влияет на позиционирование, когда overlay переходит в режим бок о бок. Эти параметры распределены по дочерним представлениям, и не все они применимы к каждому возможному результату. Поэтому разработчику нужно понимать не только сами настройки, но и то, как контейнер в итоге их интерпретирует.

С некоторых точек зрения ArrangementView ближе к контейнеру вроде NavigationSplitView, где адаптивное поведение уже встроено. Разница в том, что у NavigationSplitView есть ясная навигационная семантика, явное состояние видимости и хорошо определённое поведение при схлопывании содержимого — например, в компактной ширине он превращается в одноколоночную стековую навигацию. ArrangementView, напротив, должен описывать более широкий спектр отношений между содержимым, из-за чего его семантическую основу сложнее определить.

Неоднозначное понятие primary и secondary

Компромиссы, которые принимает ArrangementView, происходят на уровне представлений. Когда одно из них временно исчезает, вопрос уже не только в его размере, но и в том, остаются ли доступными его информация и взаимодействия.

В split-компоновке смысл primary довольно очевиден: если ограничения не позволяют сохранить разделение, именно primary-представление получает приоритет и остаётся видимым.

Но в overlay тот же термин получает другую роль: когда контейнер использует многослойную компоновку, primary-представление оказывается поверх secondary. Это отличается от привычной семантики модификатора overlay. В случае обычного модификатора поверх исходного представления добавляется новый слой, а исходное остаётся снизу. В overlay внутри ArrangementView, напротив, primary означает именно верхний слой.

Из этого следует, что представление, которое логично оставить видимым как primary в split, вовсе не обязательно должно быть верхним представлением в overlay.

Собственный пример Apple хорошо показывает эту разницу. При переходе от split к overlay компания меняет местами роли primary и secondary у PlayerView и UpNextView.

Проблема не в том, что какое-то из этих правил нелогично. Проблема в том, что два разных набора правил используют одну и ту же терминологию primary/secondary, хотя смысл этих ролей приходится определять по-разному в зависимости от текущего типа компоновки. Primary и secondary выглядят так, будто задают единое отношение между содержимым, но на деле их смысл всё ещё зависит от того, какой arrangement используется прямо сейчас.

Один контейнер, две логики

Объединить split и overlay в одном контейнере — не совсем нелогичное решение. Когда overlay сталкивается с активной областью разделения устройства, он и сам может перейти в режим бок о бок, визуально становясь очень похожим на split.

Но похожая геометрия не означает одинакового отношения между содержимым.

Особенно хорошо это видно при переключении между режимами. Если продукт должен переходить между split и overlay, разработчику всё равно приходится решать, когда должен происходить переход, нужно ли менять primary и secondary местами и как при этом сохранять состояние содержимого.

В SwiftUI .split и .overlay — разные конкретные типы. Для переключения между ними нужен if else, а значит, меняется и идентичность представления. Даже если завернуть выбор в собственный ArrangementViewStyle и выбирать режим по значению внутри makeBody, по сути это всё равно ещё один слой вокруг if else; добавленная анимация даст переход между состояниями, но не непрерывное преобразование одного в другое.

Я также протестировал updateArrangement(_:animated: true) в UIKit. Хотя экземпляр контейнера остаётся тем же, в Xcode 27.1 beta результат всё равно выглядит как жёсткое переключение.

По крайней мере сейчас непрерывного перехода между split и overlay нет. Их объединили внутри одного контейнера, но от этого они не стали единым непрерывным состоянием компоновки. На практике ArrangementView ощущается скорее как объединённые в одном контейнере ArrangementSplitView и ArrangementOverlayView.

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

Нет единого источника истины

Когда контейнер сам отвечает за определение итоговой компоновки, возникает естественный вопрос: могут ли его дочерние представления узнать, какое решение он в итоге принял?

Вокруг ArrangementView Apple предоставляет такие значения, как overlayArrangementZIndex, splitArrangementAxis и reservedRegions для получения зарезервированных областей устройства. Каждое из них полезно, но в основном они раскрывают отдельные кусочки информации, участвующие в процессе компоновки, а не полный итоговый результат контейнера.

Разработчик может узнать ось split, порядок слоёв overlay и зарезервированные области устройства. Но гораздо сложнее напрямую ответить на более общий вопрос: какой именно arrangement пользователь видит сейчас?

Например, значение overlayArrangementZIndex равное 0 само по себе не говорит, находится ли представление в нижнем слое или overlay уже перешёл в режим бок о бок. Аналогично, splitArrangementAxis описывает ось разделения, но не полное состояние видимости. Разумеется, разработчик может вывести итог из дополнительного контекста, однако тогда приложению приходится самостоятельно собирать воедино несколько разрозненных сигналов.

Я бы предпочёл видеть централизованное состояние, уже разрешённое самим контейнером — например: split с указанием его оси, overlay с информацией о слоях или состояние, в котором сейчас отображается только primary-представление.

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

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

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

Его сильная сторона — одновременно и его граница

Если воспринимать ArrangementView как универсальный контейнер, описанные выше издержки быстро становятся заметнее. Но стоит поместить его в контекст iPhone Duo, как некоторые на первый взгляд странные правила сразу получают вполне конкретный смысл.

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

Это также объясняет ту часть моего первоначального дискомфорта, которая возникла из-за неправильного понимания роли API. Некоторые проблемы вовсе не были тем, что API не сумел решить; просто я ожидал от него слишком многого.

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

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

На мой взгляд, ArrangementView стоит рассматривать, когда интерфейсу действительно нужен собственный split или overlay. Перед его использованием я бы задал несколько более конкретных вопросов:

  1. Есть ли у двух представлений стабильное отношение «основное содержимое / детали» или «передний план / фон»?
  2. Когда пространства становится мало, какое представление может исчезнуть? Когда они перекрываются, какое должно находиться сверху? Можно ли выразить оба ответа одной и той же семантикой primary/secondary?
  3. Нужны ли дочерним представлениям лишь небольшие изменения в зависимости от итоговой компоновки, или большей части содержимого приходится самостоятельно определять и реконструировать текущий режим?
  4. Действительно ли системная обработка областей, связанных со сгибом устройства, устраняет решения по компоновке, которые в противном случае пришлось бы поддерживать самому приложению?

Если ответы очевидны — особенно когда интерфейс уже похож на один из двух типов отношений, которые демонстрирует Apple, — ArrangementView может оказаться очень удобным инструментом. Если же ответы неоднозначны или само переключение между этими отношениями является важной частью логики продукта, я бы скорее начал с HStack, VStack, ZStack, стандартных навигационных контейнеров или собственной компоновки.

Это вовсе не означает, что вы отказываетесь от какого-то «более продвинутого» подхода. Вы просто оставляете решение там, где лучше всего понимают само содержимое.

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

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

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: