Часть 3: Написание DSL с автоматической компоновкой с перегрузкой операторов Swift и построителями результатов ️

Nov 26 2022
Вы также можете посмотреть эту статью здесь: В прошлый раз мы построили основную часть DSL, позволяя конечному пользователю выражать ограничения арифметическим способом. В этой статье мы расширим это и включим: Сначала мы создаем тип для хранения пары якорей, которые соответствуют протоколу LayoutAnchor, определенному в части 1.

Вы также можете посмотреть эту статью здесь:

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

В этой статье мы расширим это, включив в него:

  • Комбинированные привязки — размер, центр, горизонтальные края, вертикальные края
  • Вставки — разрешить пользователям устанавливать вставки на комбинированных привязках ребер.
  • Result Builder для обработки вывода различных привязок, что позволяет легко группировать ограничения и активировать их.

Сначала мы создаем тип для хранения пары якорей, которые соответствуют протоколу LayoutAnchor, определенному в части 1.

Здесь я подумал о том, чтобы изменить LayoutBlock для хранения массива якорей. Затем, при создании ограничения из 2 таких блоков, мы могли бы соединить якоря вместе и перебирать их, ограничивая якоря друг другом и передавая соответствующие константы/множители.

Есть 2 недостатка:

  • Даже отдельные выражения с базовыми якорями будут возвращать массив ограничений. Это усложняет DSL с точки зрения пользователей.
  • Пользователь может попробовать использовать составную привязку (с 2 или 4 привязками) с одинарной привязкой. Мы можем справиться с этим, игнорируя дополнительные якоря. Компилятор не выдаст предупреждения. Однако эти операции и результирующие ограничения будут бессмысленными. Это может привести к разочаровывающим ошибкам в коде конечных пользователей — чего мы хотим избежать!

Шаг 11: Расширьте протокол LayoutAnchorPair.

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

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

Поскольку привязки, которые мы передаем в LayoutAnchorPair, ограничены типами LayoutAnchor, мы можем легко предоставить реализацию по умолчанию.

Кроме того, вместо констант эти методы принимают EdgeInsetPair, который дает возможность задавать разные константы для каждого из ограничений.

Каждый метод сопоставляет константу1 с ограничением привязки1 и константу2 на привязку2.

Шаг 13: Создайте конкретные типы LayoutAnchor.

В части 1 и 2 нам не нужно было создавать конкретные типы LayoutAnchor, так как мы просто сделали стандартные NSLayoutAnchors соответствующими протоколу LayoutAnchor. Однако здесь нам нужно предоставить собственные якоря, соответствующие протоколу AnchorPair.

Напоминание о typealiases, использовавшихся ранее:

Мы определяем вложенный тип, который удовлетворяет протоколу EdgeInsetPair. Это удовлетворяет требованию связанного типа AnchorPair — Insets. Конкретный тип, соответствующий этому протоколу, будет использоваться во время перегрузки операции для установки вставок.

Мы используем вычисляемые свойства для соответствия протоколам LayoutAnchorPair и EdgeInsetPair. Эти вычисляемые свойства возвращают внутренние свойства LayoutAnchorPair и EdgeInsetPair.

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

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

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

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

Еще точки архитектурного решения:

  • Вставки должны быть отдельными типами, поскольку они инициализируются и используются вне привязки.
  • Вложенные типы используются для защиты пространства имен и поддержания его чистоты, а также подчеркивают тот факт, что реализация типа Inset зависит от конкретной реализации PairAnchor.

Примечание. Добавление вставок — это не то же самое, что добавление констант в выражение.

Вы могли заметить, что мы добавили оператор минус к верхней константе, прежде чем возвращать ее как часть интерфейса EdgePair. Точно так же реализации XAxisAnchorPair добавляют минус к замыкающей привязке. Инвертирование констант означает, что вставки будут работать как вставки, а не сдвигать каждое ребро в одном и том же направлении.

Красный вид ограничен до 200 на первом и втором изображениях.

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

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

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

Шаг 14. Расширьте View & LayoutGuide, чтобы инициализировать составные привязки.

Как и раньше, мы расширяем типы View и LayoutGuide вычисляемыми свойствами, которые инициализируют LayoutBlocks при вызове.

Шаг 15: Перегрузите операторы +/-, чтобы разрешить выражения с краевыми вставками.

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

Расширение позволяет инициализировать только верхние/нижние или левые/правые вставки — остальные по умолчанию равны 0.

Нам также нужно добавить новое свойство в LayoutBlock для хранения edgeInsets.

Далее мы перегружаем операторы для ввода: LayoutBlock с UIEdgeInsets .

Мы сопоставляем экземпляр UIEdgeInsets , предоставленный пользователем, с соответствующим вложенным типом, определенным как часть конкретного LayoutAnchorPair .

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

Шаг 15: Перегрузите операторы сравнения, чтобы определить отношения ограничений.

Принцип остается тем же, что и раньше. Мы перегружаем операторы отношений для входных данных LayoutBlocks и LayoutAnchorPair .

Если пользователь предоставляет пару краевых вставок — мы их используем, в противном случае мы генерируем пару универсальных вставок из констант LayoutBlocks . Общая структура вставки является оберткой, в отличие от других вставок, она не отрицает одну из сторон.

Шаг 16: Измерение LayoutAnchorPair

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

  • Разрешить пользователю DSL фиксировать высоту и ширину одной и той же константой — создавая квадрат.
  • Разрешить пользователю DSL привязывать SizeAnchorPair к типу CGSize — в большинстве случаев имеет смысл, поскольку представления не являются квадратами.

Шаг 17: Использование построителей результатов для обработки типов [NSLayoutConstraint] и NSLayoutConstraint.

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

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

Представленные в Swift 5.4 построители результатов (также известные как построители функций) позволяют вам создавать результат, используя «блоки построения» неявно из ряда компонентов. Фактически, они являются базовым строительным блоком пользовательского интерфейса Swift.

В этом DSL конечным результатом является массив объектов NSLayoutConstraint .

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

Большинство этих функций я скопировал непосредственно из примера предложения конструктора результатов swift-evolution . Я добавил модульные тесты, чтобы убедиться, что они правильно работают в этом DSL.

Если сложить все это, получится следующее:

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

Вот и все, спасибо за чтение! Написание этой статьи заняло много дней, поэтому, если вы узнали что-то новое, я был бы признателен за ⭐ в этом репозитории!

Если у вас есть для меня совет, не стесняйтесь: и делитесь своим опытом!

Окончательная версия этого DSL включает в себя четырехмерную привязку, состоящую из пары типов AnchorPair

Вы можете найти весь код здесь: