Как программно написать ограничение AutoLayout в приложении MVVM.

При разработке приложения для iOS с помощью UIKit нам нужно решить, как расположить наши представления. Существуют три альтернативы: файлы раскадровки / XIB, программная автоматическая компоновка или настройка рамок представления вручную. Среди этих трех я мало знаю о программном AutoLayout, и я хотел узнать, как это работает.

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

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

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

  1. Они скрывают, как все работает на самом деле. Если есть что-то не так или визуальная ошибка, обычно требуется много времени, чтобы понять это и исправить с помощью визуального файла.
  2. Они имеют версии как XML с настраиваемым синтаксисом. Из-за этого очень сложно сотрудничать и разрешать конфликты в команде.
  3. Раскадровки и XIB требуют хранения и использования дополнительных файлов ресурсов в проекте.

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

Моя эталонная архитектура при работе с приложением - это MVVM: этот высоко декларативный подход должен быть хорошим помощником для декларативного характера ограничений AutoLayout.

Давайте вместе посмотрим, как создать макет представления с помощью этой технологии.

AutoLayout: основы

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

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

Примечание: некоторые представления, такие как UILabel и UIButton, имеют внутренний размер, который зависит от содержимого, которое они содержат. Это называется intrinsicContentSize. В этих случаях мы можем установить только два ограничения, вертикальное и горизонтальное, потому что два других вытекают из размера компонента.

Горизонтальные ограничения могут быть одним из следующих пар:

С одной из этих пар система знает, как расположить наш вид относительно горизонтальной оси.

Вместо этого вертикальные ограничения могут быть взяты из следующего списка:

С одной из этих пар система знает, как расположить наш вид относительно вертикальной оси.

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

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

Типичные варианты использования:

  • Родительское представление: например, заголовок может быть выровнен по ведущим, верхним и конечным якорям родительского элемента и может иметь собственную высоту.
  • К родственным представлениям: например, конечная привязка UILabel может быть прикреплена к ведущей привязке UIImageView в ячейке табличного представления.
  • К специальному руководству по содержанию: каждое представление предлагает несколько специальных руководств по макету, которые автоматически реализуют некоторые настройки по умолчанию, предлагаемые Apple. Самым распространенным из них является SafeAreaLayoutGuide, который можно использовать для соблюдения выемки в iPhone.

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

И это почти. Не забывайте еще две важные детали:

  1. Перед установкой любого ограничения нам нужно установить для свойства translatesAutoresizingMaskIntoConstraints каждого представления значение false.
  2. Нам нужно активировать ограничения. Это можно сделать двумя способами: установив для свойства isActive каждого ограничения значение true или используя метод NSLayoutConstraint.activate(), который требует список ограничений в качестве параметра.

И это все, что нам нужно. Давайте посмотрим на них на практике.

Ограничения на практике

Для этой статьи я создал очень простой проект. Проект имеет классическую структуру MVVM: у нас есть Data модель, ViewController, ViewModel и View.

Модель данных

Модель данных довольно проста. Как фанат League of Legend, я нарисовал несколько моих любимых чемпионов с их веб-сайта и смоделировал их в эти структуры:

У каждого чемпиона есть name, fileName, которые относятся к ресурсу, который я скопировал в проект, description и type, которые смоделированы с использованием enum Kind с удобочитаемым полем description.

ViewController

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

ViewModel

Для ViewModel я решил использовать структуру Combine для реализации привязки между собой и представлением.

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

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

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

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

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

Вид

Наконец, давайте посмотрим, как реализовано представление.

Мне нравится организовывать представление, используя очень конкретный шаблон: я объявляю свои элементы управления и представления сверху, я добавляю их к родительскому представлению с помощью метода setup и стилизую их с помощью метода style.

В init, в строке 13, у нас есть пара других интересных методов: setupBindings и setupConstraints.

setupBinding используется для соединения пользовательского интерфейса с ViewModel. На практике он настраивает подписки на объединение таким образом, чтобы каждый раз, когда издатель отправляет новое значение, представление могло обновлять свое содержимое.

В этом коде есть два интересных момента:

  1. Мы не получаем явно в основном потоке. В этом конкретном примере это безопасно: свойство @Published отправляет свои значения синхронно и в том же потоке, который инициировал публикацию. Из @Published документации: «Когда свойство изменяется, публикация происходит в блоке willSet свойства, то есть подписчики получают новое значение до того, как оно будет фактически установлено для свойства».
  2. В строке 12 мы обновляем ограничения. Это важный шаг, если у нас есть представления с внутренним размером содержимого: если их содержимое изменяется, мы действительно хотим пересчитать ограничение, чтобы приветствовать новый контент. Считайте это эквивалентом setNeedsLayout для AutoLayout.

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

Блок со строки 4 по строку 13 используется для установки свойства translatesAutoresizingMaskIntoConstraints на false для всех представлений, включенных в текущее представление.

Затем у нас есть несколько NSLayoutConstraint.activate методов для установки всех ограничений. Мы не будем вдаваться в подробности всех из них, потому что они очень похожи друг на друга. Каждое ограничение структурировано в следующей форме:

self.<subject_view>.<anchor>.constaint(relationshipWith: <reference_view>.<other_anchor>)

Для каждого предметного представления мы устанавливаем якоря на какой-либо другой якорь других представлений в иерархии.

Метод NSLayoutConstrain.activate() позаботится о том, чтобы активировать их все.

За и против

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

Еще один недостаток - многословие. Чтобы решить эту проблему, мы могли бы приготовить несколько хороших утилит для наших представлений. Например, мы можем написать что-то вроде этого:

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

Подход имеет ряд преимуществ:

  1. Он автоматически обрабатывает некоторые действия, такие как поворот устройства.
  2. Нам не нужно переопределять метод layoutSubviews и иметь фрагменты кода, которые запускаются при каждом цикле обновления.
  3. Нам не нужно выполнять сложные вычисления с кадрами и расстоянием между видами.
  4. Это более декларативно.

Результат

Это окончательный результат нашей функции MVVM:

Обратите внимание, как метка имени и метка типа обновляются автоматически при изменении Champion, без отображения какого-либо многоточия.

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

Заключение

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

Мы обсудили, как использовать его в реальном приложении, разработанном с использованием архитектуры MVVM на базе Combine.

Наконец, мы обсудили его общие плюсы и минусы по сравнению с другим подходом, ориентированным на код: установка фреймов вручную.