NSLayoutConstraints, SnapKit и язык визуального формата

Теперь, когда доступна iOS 13, разработчики могут начать использовать SwiftUI в своих приложениях. Но, возможно, вы не из тех счастливчиков, которые могут начать с зеленого поля и вам нужно поддерживать больше, чем последняя версия iOS.

Независимо от того, работаете ли вы над большим приложением, которое уже разрабатывалось в течение нескольких лет, или ваши пользователи все еще в основном используют iOS 12, есть много веских причин, по которым UIKit по-прежнему будет актуален в следующем году или около того.

Работа с NSLayoutConstraints в раскадровках или XIB также может быть довольно болезненной, поскольку файлы могут становиться большими и хаотичными, чем требуется больше ограничений. Если вы работаете с контролем версий, вы будете проклинать каждую запутанную разницу между текущей версией и новой версией и ненавидите конфликты слияния.

Итак, что же остается, если SwiftUI еще не является хорошим решением, а раскадровки подходят не всем? Что ж, есть еще возможность использовать NSLayoutConstraints в коде или использовать язык визуального формата, Apple DSL, для компоновки представлений.

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

Структура представления

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

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

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

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

Вы можете ссылаться на эти свойства, используя NSLayoutAnchors. Есть якоря для оси x (так называемые NSLayoutXAxisAnchors), содержащие leadingAnchor, trailingAnchor, leftAnchor, rightAnchor и centerXAnchor.

И якоря для оси Y (NSLayoutYAchsisAnchors), это topAnchor, bottomAnchor, firstBaselineAnchor, lastBaselineAnchor и centerYAnchor.

Два базовых якоря предназначены для использования в UITextViews. Кроме того, есть два NSLayoutDimensions, widthAnchor и heightAnchor, а также UILayoutGuides, layoutMarginsGuide, readableContentGuide и safeAreaLayoutGuide, которые были введены в iOS 11 для обработки пользовательских интерфейсов на iPhone без кнопки «Домой».

Все эти привязки, размеры и направляющие позволяют нам полностью определять положение и размер представления на экране.

Примеры

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

  1. Размещение представления в его супервизоре.
  2. Изменение размера представления.
  3. Позиционирование вида по отношению к другому виду.
  4. Разрешение конфликтов и использование приоритетов.
  5. Анимация изменений.

Сначала мы рассмотрим, как это сделать с помощью простого NSLayoutConstraints. В следующем разделе мы рассмотрим, как SnapKit может помочь нам улучшить наш код, и, наконец, мы увидим, как можно использовать язык визуального формата.

Добавление NSLayoutConstraints

Начнем с первого примера: как разместить представление в его суперпредставлении.

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

// 1 - Мы создаем простой квадратный вид с белым цветом фона. Это представление будет использоваться как супервизор, в котором будут храниться все остальные представления.

// 2 - Мы создаем новое представление, на этот раз с зеленым цветом, чтобы различать его и его супервизор. Нам также необходимо установить для его свойства translatesAutoresizingMaskIntoConstraints значение false, указывая, что мы будем использовать автоматический макет и ограничения.

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

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

В этом примере мы добавляем ограничения, чтобы разместить наше представление по центру внутри его супервизора с расстоянием 20 с каждой стороны. Мы используем подпредставления topAnchor, leadingAnchor, bottomAnchor и trailingAnchor и размещаем их на 20 точек рядом с соответствующими якорями супервизора.

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

Теперь давайте изменим размер нашего представления!

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

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

// 1 - Сначала мы размещаем представление в контейнере. Это почти идентично первому примеру, за исключением того, что мы даем немного меньше места для trailingAnchor и bottomAnchor, чтобы разместить рядом с ним другое представление.

// 2 - Теперь мы можем добавить второй вид рядом с предыдущим. Присваивая свой topAnchor firstView topAnchor, оба начинают с той же высоты, что и их супервизор. Мы также устанавливаем leadingAnchor по отношению к trailingAnchor другого представления и уменьшаем его размер вдвое.

Опять же, создание ограничений легко, но очень многословно!

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

// 1 - Как всегда, мы начинаем с простых ограничений, чтобы разместить наше представление в контейнере.

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

// 3 - Таким образом, нам нужно расставить приоритеты. Мы можем использовать любое значение от 0 до 1000, но есть три значения по умолчанию, которые мы можем использовать. UILayoutPriority.required соответствует 1000, максимально возможному значению, .defaultHigh равно 750, а .defaultLow означает 250.

К приоритетам относятся еще два значения: ContentHuggingPriority и ContentCompressionResistancePriority.

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

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

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

Вот - отличное изображение, показывающее разницу между этими двумя приоритетами.

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

И последнее, но не менее важное: давайте оживим изменения NSLayoutConstraints.

// 1 - Как и раньше, мы добавляем ограничения к нашему представлению.

// 2 - Но на этот раз мы сохраним одно ограничение в переменной. Это то, что мы хотим изменить позже. Не забудьте его активировать!

// 3 - Поскольку мы сохранили это ограничение в переменной, мы можем изменить его позже. Здесь мы меняем свойство константы с 20 на 50, поэтому topAnchor переместится вниз на 30 пунктов.

// 4 - Мы можем анимировать это изменение, используя статический метод анимации UIView. Внутри замыкания мы вызываем layoutIfNeeded, это запустит анимацию.

Улучшение ограничений с помощью SnapKit

Теперь, когда вы узнали, как использовать NSLayoutConstraints в своем коде, вы можете предпочесть не использовать их, потому что они очень подробны. Но не волнуйтесь, у других были те же проблемы - и они создали SnapKit.

Эта структура значительно упрощает создание ограничений с гораздо меньшим количеством кода.

Посмотрите на этот первый пример, он делает то же самое, что и первый пример для NSLayoutConstraints: помещает простой вид в его супервизор с расстоянием 20 точек с каждой стороны.

Кроме того, нам не нужно устанавливать translatesAutoresizingMaskIntoConstraints для представления false, а это значит, что мы больше не можем его забыть!

Тот же результат может быть получен с меньшим количеством кода:

Удивительный! Это выглядит намного чище и приятнее, чем использование простого NSLayoutConstraints. Но это еще не все. Здесь мы делаем второй пример и даем виду высоту и ширину 100.

Когда мы хотим расположить два представления рядом друг с другом, мы также можем ссылаться на свойства SNP других представлений.

В этом примере показаны два других интересных момента:

// 1 - Мы можем указать расстояния с помощью UIEdgeInsets. firstView теперь имеет расстояние 20 пунктов вверх и слева от своего супервизора и расстояние 100 пунктов справа и снизу.

// 2 - Объединив методы dividedBy или multipliedBy, мы можем ссылаться на другие якоря и манипулировать ими одновременно.

Затем давайте посмотрим, как разрешать конфликты и использовать приоритеты.

// 1 - Объединив помеченный метод, мы можем дать ограничению явное имя. Это имя будет выведено на консоль, если возникнут конфликты, связанные с этим ограничением.

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

Как и в случае обычного UILayoutConstraints, который мы видели для NSLayoutConstraints, мы можем использовать значения .required (= 1000), .high (= 750) и .low (= 250).

Кроме того, SnapKit знает приоритет .medium, который составляет 501 для macOS и 500 для iOS.

В отличие от NSLayoutConstraints, мы не можем получить доступ к ContentHuggingPriority и ContentCompressionResistancePriority представления через SnapKit.

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

В последний раз рассмотрим SnapKit. Давайте анимируем изменения ограничений представления.

// 1 - Сначала мы добавляем представление, которое хотим анимировать, с его начальными ограничениями.

// 2 - И снова мы можем использовать статический метод UIView animate. Вызывая метод updateConstraints внутри закрытия анимации, мы можем дать ограничениям новое смещение.

// 3 - Как и раньше, мы можем запустить обновление, вызвав layoutIfNeeded.

Использование языка визуального формата

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

VFL состоит из нескольких компонентов:

  • Ориентация может быть выражена с помощью H для горизонтального или V для вертикального.
  • Супервизор представлен |.
  • Расстояние по умолчанию можно использовать со знаком -.
  • Чтобы использовать любое произвольное расстояние x, используйте -x-.
  • Значения можно сравнить с ==, >= или <=.
  • Приоритеты обозначены @.
  • Чтобы ссылаться на представление, вам нужно встроить его в [].

Давайте рассмотрим простые примеры, чтобы привыкнуть к этим обозначениям.

Строка “H:|-[button]-|” определяет горизонтальные ограничения, где представление, называемое button, имеет начальное и конечное стандартные расстояния до своего супервизора.

Другая допустимая строка - „V: |-20-[image]-20-[button]-|“. Он выражает вертикальные ограничения между видом с именем image и видом с именем button.

Верхняя привязка изображения находится на 20 точек ниже ее супервизора, а между нижней привязкой изображения и верхней привязкой кнопки снова 20 точек. Наконец, кнопка находится на расстоянии 8 точек от своего супервизора.

Вы можете ознакомиться с Учебником Рэя Вендерлиха по VFL для более подробного ознакомления с VFL.

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

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

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

// 1 - Чтобы использовать наши представления внутри строки VFL, нам нужно создать словарь, содержащий наши представления с идентификатором.

Этот словарь позже будет передан методу constraints(withVisualFormat:metrics:views:), который создает все ограничения для данной строки VFL. Метрики параметра ожидают другого словаря, который мы увидим в следующем примере.

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

// 3 - Наконец, мы можем создавать и активировать ограничения. Это можно сделать с помощью ранее упомянутого метода constraints(withVisualFormat:metrics:views:), который возвращает массив NSLayoutConstraints. Нам также необходимо активировать эти ограничения, чтобы сделать их активными.

Теперь мы можем посмотреть, как придать виду определенный размер. В приведенном ниже коде мы делаем почти то же самое, что и раньше: сначала мы создаем views-dictionary, затем мы указываем ограничения в строке VFL, просто чтобы создать и активировать ограничения на последнем шаге.

Вы также увидите эту процедуру в следующих примерах. Основное различие здесь - две струны. Чтобы задать для вида значение высоты и ширины, вы можете указать размер в скобках.

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

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

// 1 - Мы начинаем с создания словаря, содержащего первое представление, определения строк, размещения первого представления внутри его супервизора, а также создания и активации этих ограничений.

// 2 - Чтобы добавить второе представление, нам нужно обновить словарь представления, чтобы он также содержал это второе представление.

// 3 - На этот раз нам также понадобится словарь метрик, в котором будут храниться значения, которые мы хотим использовать в наших строках VFL. В этом случае мы хотим дать второму виду половину высоты первого представления, поэтому мы делаем это значение доступным под идентификатором halfHeight.

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

// 5 - Наконец, мы можем создать и активировать все ограничения для второго вида. Здесь мы также передаем в метод словарь метрик.

Это снова так много работы, чтобы просто показать два представления рядом друг с другом! Он может выглядеть немного чище, чем при использовании NSLayoutConstraints, но такой же подробный.

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

Как всегда, мы можем использовать числа от 0 до 1000, но на этот раз нет констант, которые мы могли бы использовать.

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

// 2 - Теперь мы можем создавать строки VFL. Здесь мы говорим, что у вида не должно быть расстояния между левой и правой стороной и левой и правой стороной супервизора. Кроме того, мы ожидаем, что это будет половина ширины контейнера - конечно, это не сработает. Таким образом, нам нужны приоритеты, которые добавляются сразу после конфликтующих ценностей.

// 3 - В качестве последнего шага мы создаем и активируем эти ограничения.

И последнее, но не менее важное: давайте посмотрим, как анимировать изменения.

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

// 2 - Изменение ограничений с помощью VFL - это большая работа. Во-первых, нам нужно деактивировать старые ограничения.

// 3 - Затем нам нужно создать и активировать новые ограничения, мы не можем повторно использовать старые.

// 4 — Чтобы запустить анимацию, мы используем layoutIfNeeded в закрытии метода animate, как всегда.

Заключение

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

VFL позволяет более компактно описать наш пользовательский интерфейс, но может стать весьма подверженным ошибкам из-за использования строк. Кроме того, он не менее подробен, чем предыдущий.

SnapKit сочетает в себе компактное описание VFL с более простым в использовании NSLayoutConstraints и, таким образом, кажется лучшим вариантом, если вы хотите создавать свои представления в коде.

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

Вот и все! Теперь вы увидели несколько вариантов использования, реализованных с использованием чистого NSLayoutConstraints, SnapKit и языка визуальных форматов.

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

Ресурсы