В прошлой статье мы реализовали представление панели канала, которое было адаптировано под все модели iPhone. Для реализации данного функционала был использован специальный механизм Xcode — Auto Layout.
Auto Layout — подход от Apple. Имеет API в виде системы ограничений (constraints).
Он динамически вычисляет размер и положение всех графических объектов в иерархии UIView, основываясь на ограничениях, указанных для этих объектов. Самый большой и очевидный плюс в использовании Auto Layout в том, что исчезает необходимость в подгонке размеров приложения под определенные устройства — Auto Layout делает это самостоятельно, динамически изменяя интерфейс в зависимости от экрана и его изменений, таких как изменение ориентации экрана, изменение контента в окне и др. Таким образом, подобный подход к проектированию позволяет создавать гибкий интерфейс.
Верстка
Необходимо понимать перед тем, как приступить к проектированию интерфейса: чтобы однозначно задать положение объекта на экране, нужно предусмотреть его 4 параметра:
- Координаты начальной точки (левой верхней): положение по оси Х и положение по оси Y.
- Размеры: длину (width) и высоту (height).
Итоговые 4 параметра (значения) - фрейм объекта. Предположим, что эти параметры — решение некоторой системы линейных уравнения.
Система линейных уравнений особенна тем, что над ней определена масса операций: складывание строк, умножение их на константы и т.д. Эти операции называются линейными преобразованиями, и с их помощью система приводится к произвольной форме.
Получается, алгоритм верстки выглядит следующим образом:
- составляем систему линейных уравнений/неравенств;
- решаем ее;
- применяем решение к заданному представлению.
Данный алгоритм верстки еще называют алгоритмом Cassowary. Именно его использует Auto Layout, чтобы рассчитать итоговые фреймы заданных объектов. Если рассматривать его подробнее, то выглядит он таким образом:
- определяются необходимые ограничения — требования к верстке;
- составляется система линейных ограничений;
- все ограничения преобразуются в задачу линейного программирования;
- решается последовательно задача линейного программирования — от наиболее приоритетной к наименее приоритетной симплекс-методом.
- полученное решение применяется к UlView (получаем итоговые frames).
Проектирование интерфейса с помощью ограничений
Создадим новый проект, который будет называться AutoLayoutProject.
В нем мы избавимся от Storyboard и Xib(Nib) файлов. Будем верстать все исключительно в коде, поскольку тогда мы не теряем время на парсинге файлов и получаем большую гибкость проектирования интерфейса.
Для того, чтобы удалить Main.storyboard из проекта, давайте выберем его в Project Navigator и нажнем клавишу Delete.
Далее нажимаем Move to Trash, таким образом удаляя файл из нашего проекта.
Затем переходим в сам проект в Project Navigator и останавливается на вкладке General в окне Project Editor в настройках Target. Тут видим раздел Deployment Info, в котором необходимо удалить название файла Main из поля Main Interface. Оно должно быть пустым.
Также необходимо удалить поле Storyboard Name в Info.plist.
Теперь в проекте не используется Storyboard.
Но какой контроллер теперь наше приложение будет загружать первым? Ведь имея сториборд в проекте и указав его имя в файле Info.plist, главная функция приложения UIApplicationMain создает экземпляр UIWindow и назначает его делегатом сцены, далее вызывает отображение интерфейса, устанавливая Root View Controller, являющийся стартовой сценой сториборда, путем вызова метода экземпляра UIWindow makeKeyAndVisible(). А затем в делегате сцены уже вызывается метод scene(_:willConnectTo:options:). Получается, не указав имя сториборда в файле Info.plist, UIApplicationMain не создаст экземпляр UIWindow в проекте, а значит, ничего не отобразит на экране. Именно поэтому создадим экземпляр UIWindow вручную в методе делегата сцены scene(_:willConnectTo:options:), тем самым обеспечив окно для взаимодействия пользователя с приложением.
А во ViewController настроим фон корневого вью.
Проверим, что стартовый экран загружается.
Следующий задачей стоит создание представления ChannelHeaderView и отображение его на экране контроллера.
Создадим ChannelHeaderView.swift файл.
Создадим класс ChannelHeaderView, наследованный от UIView, и отрисуем представление согласно макету из предыдущей статьи.
Иерархию представления соблюдаем следующую.
Супервью (корневое вью view) - разрабатываемый нами объект типа ChannelHeaderView. Он имеет 3 subviews: горизонтальный стек желтого цвета channelInfoStackView, лейбл с описанием канала зеленого цвета channelDescriptionLabel и кнопку синего цвета editButton. Горизонтальный стек channelInfoStackView в свою очередь имеет 2 своих subviews: вертикальный стек, объединяющий лейбл с названием канала и 3 лейблами с его статистикой channelLabelsStackView и картинку красного цвета avatarImageView. Вертикальный стек channelLabelsStackView в свою очередь имеет 2 своих subviews: лейбл с названием канала цвета индиго channelNameLabel и горизонтальный стек с 3 лейблами голубого цвета channelDigitsStackView. И наконец горизонтальный стек голубого цвета channelDigitsStackView имеет 3 своих subviews: лейбл фиолетового цвета postsNumberLabel, лейбл коричневого цвета subscribersNumberLabel и лейбл серого цвета audienceNumberLabel.
Первым делом создадим UI-компоненты, которые будут отображаться на нашем кастомном вью. Для этого используем приватные ленивые переменные private lazy var.
Lazy-переменные похожи на обычные переменные, за исключением того, что они не инициализируются (не занимают какое-либо пространство памяти) до тех пор, пока они не будут вызваны в первый раз. Это означает, что ленивые переменные не инициализируются при инициализации представления, а ожидают более позднего момента, когда они действительно необходимы, что экономит вычислительную мощность и пространство памяти для других процессов. Это особенно полезно в случае инициализации компонентов пользовательского интерфейса.
Затем необходимо добавить только что созданные нами компоненты в качестве subviews к представлению с помощью метода .addSubview(_: UIView) и в стеки с помощью addArrangedSubview(_: UIView). Все это делаем в отдельной функции drawSelf(), которую нужно не забыть вызвать в конструкторе вью override init(frame: CGRect).
И наконец, перейдем к ограничениям.
Для добавления ограничения к выбранному представлению, например, ограничения слева к RedView, используются следующие варианты кода:
Таким образом, левая сторона представления RedView крепится привязкой к правому краю представления BlueView с интервалом в 8 поинтов.
Объявим ограничения для subviews представления ChannelHeaderView:
- Горизонтальный стек, включающий в себя название канала, его статистику и аватар, channelInfoStackView крепим к верху представления (то есть верх стека крепим к верху superview), а также к его левой и правой сторонам. Причем немного увеличивая интервал, не прикрепляя стек вплотную к краям экрана.
- Картинке avatarImageView обязательно зададим размер, обозначив ограничение высоты и ширины, равные 100 поинтам. Не указав их, картинка не будет определена однозначно.
- Лейблам с названием канала channelNameLabel и его статистикой (postsNumberLabel, subscribersNumberLabel, audienceNumberLabel) никаких ограничений указывать не будем, поскольку установили их супервью, стекам channelLabelsStackView и channelDigitsStackView, поле distribution в fillEqually.
- Лейбл с описанием канала channelDescriptionLabel привязываем к низу горизонтального стека channelInfoStackView, уже размещенного на супервью, а также к его левому и правому краям. Поскольку привязка производится к краям стека, а не супервью, то они уже будут иметь сдвиг, заданный сдвигом ограничений стека.
- Кнопку editButton привязываем к низу лейбла с описанием channelDescriptionLabel, причем ограничению указываем взаимосвязь между связываемым объектами (Relationship) как Greater Than or Equal для того, чтобы регулировать высоту лейбла в зависимости от его содержимого. Также привязываем кнопку к левому и правому краям лейбла с описанием и к низу супервью. Задаем высоту кнопки, равную 50 поинтам.
Теперь отобразим созданное представление на корневом вью контроллера.
В контроллере проделываем то же, что и в кастовом вью ChannelHeaderView ранее.
Иерархия корневого представления контроллера будет следующей.
Создаем UI-компонент, который будет отображаться на нашем контроллере, используя lazy-переменную.
Добавляем только что созданный компонент в качестве subviews к корневому представлению.
Объявляем следующие ограничения для кастомного представления channelHeaderView:
- Верх привязываем к верху Safe Area;
- Левый и правый края к соответствующим краям superview;
- Задаем высоту нашему представлению в 250 поинтов. На основании этого ограничения и ограничений, заданных непосредственно в самой ChannelHeaderView, Auto Layout будет пытаться разместить ее subviews.
Запустим приложение на симуляторах различных устройств и убедимся, что ограничения расставлены верно.
Осталось дело за малым: реализуем окончательный интерфейс из предыдущей статьи. Для этого создадим функцию, с помощью которой будем устанавливать данные в элементы представления. А также настроим скругление картинки и шрифты.
Вызовем функцию setupView() при появлении контроллера на экране.
Посмотрим на получившийся результат.
Видно, что результат аналогичен эталонному из предыдущей статьи, только теперь вся верстка реализована в коде.
Особенности iOS
- В layoutSubviews() нет расчетов.
Вызывается уже при наличии рассчитанных фреймов. Расчет фреймов происходит ровно тогда, когда ограничения активируются с помощью методов работы Auto Layout API.
2. intrinsicContentSize
Свойство intrinsicContentSize позволяет представлению определять, какой размер она хотела бы иметь на основе своего содержимого. По умолчанию некоторые дочерние компоненты UIView имеют intrinsicContentSize, например, UILabel. Базовый UIView не имеет этого свойства, и если попытаться его распечатать, то результат будет {-1, -1}. Ширина и высота intrinsicContentSize определяет константы констрейнтов, которые система неявно добавляет на нашу вью. Этот механизм очень удобен, он позволяет уменьшать количество явных ограничений, что упрощает использование Auto Layout.
3. translateAutoresizingMaskIntoConstraints
Если значение переменной translateAutoresizingMaskIntoConstraints равно true, то внедряется дополнительное ограничение каждой view, сверстанной на фрейме. Этот набор ограничений может отличаться от запуска к запуску. Про этот набор известно лишь одно — его решением будет именно тот фрейм, который был передан. Эти ограничения обязательно имеют приоритет требований, поэтому если вдруг на такое представление будет наложено ограничение (constraint), у которого очень высокий приоритет, например, требование, то создастся не консистентная система, которая не будет иметь решений.
Важно знать:
- Если вью создается из Interface Builder, то значение по умолчанию для этого свойства будет false.
- Если же вью создается непосредственно из кода, то оно будет true.
Ну а в следующей части мы рассмотрим такое действие в приложениях, как прокрутка, и реализуем несложный экран авторизации на основе UIScrollView.