Хочу поделиться библиотекой для эффективного построения пользовательского интерфейса iOS приложений на основе autolayout.
Хотя с появлением SwiftUI актуальность autolayout быстро уменьшается, пока этот механизм все еще активно используется, и библиотека может быть полезна для тех, кто создает (или меняет) UI непосредственно в коде.
У такого способа построения интерфейса есть ряд недостатков, которые ограничивают его применение:
Первая проблема достаточно легко решается обертыванием стандартных методов создания constraints во что нибудь более гуманное. И это уже отлично реализовано, например, в SnapKit, TinyConstraints и других подобных библиотеках.
Но все равно приходится писать достаточно много однотипного кода, и остаются проблемы с наглядностью и динамическим изменениями лейаута. Эти проблемы изящно решает UIStackView, но к сожалению, в UIStackView очень ограничена настройка расположения отдельных элементов. Поэтому возникла идея контейнерной UIView, управляющей лейаутом стека своих subview, но с возможностью индивидуальной настройки расположения каждой subview.
Именно этот подход лежит в основе BoxView, и он оказался очень эффективным. BoxView позволяет почти полностью исключить ручное создание constraints, практически весь пользовательский интерфейс формируется в виде системы вложенных BoxView. В итоге код стал намного короче и нагляднее, выигрыш особенно ощутим для динамических UI.
BoxView во многом похожий на стандартный UIStackView, но для размещения subview он использует другие правила: в нем можно устанавливать отступы и размеры для каждой subview индивидуально. Для создания лейаута BoxView использует массив элементов типа BoxItem, который содержит все view которые надо отобразить, и информацию о том как их расположить. И для этого совсем не требуется много кода — большая часть параметров лейаута берется по умолчанию, а явно указываются только нужные значения.
Существенное свойство BoxView в том, что он создает только указанные constraints для добавленных subview, и ничего более. Поэтому его можно использовать без каких либо ограничений совместно с любыми другими библиотеками и методами лейаута.
В качестве примера рассмотрим создание простой логин формы с помощью BoxView (Полный код примера с пошаговым описанием доступен в проекте BoxViewExample на github).
Для создания такого лейаута на BoxView достаточно нескольких строк кода:
Элемент BoxItem создается из любой UIView с помощью переменной boxed, после этого ему можно задать отступы с 4 сторон, выравнивание, а также абсолютные или относительные размеры.
Любые элементы лейаута можно свободно добавлять и удалять (в том числе с анимацией), не затрагивая размещения остальных. В качестве примера добавим проверку на пустые поля ввода и в случае ошибки будем показывать сообщение непосредственно под пустым полем:
И хотя сообщение должно «встроиться» в существующий лейаут, для этого даже не потребуется менять имеющийся код!
BoxView поддерживает весь инструментарий autolayout: расcтояния между элементами, абсолютные и относительные размеры, приоритеты, поддержку RTL языков. Помимо UIView в качестве элементов лейаута можно также использовать невидимые объекты — UILayoutGuides. Можно использовать также flex layout. Разумеется сама схема лейаута, в виде системы вложенных стеков UIView, не покрывает на 100% все мыслимые варианты относительного расположения элементов, но этого и не требуется. Она как раз хорошо походит для подавляющего большинство типичных пользовательских интерфейсов, а для более экзотических случаев, всегда можно добавить соответствующие дополнительные constraints любым другим способом. Несколько утилитных методов, например, для создания aspect ratio constraints также включены в библиотеку.
Еще один небольшой пример доступный на github (~100 строк кода!) иллюстрирует использование системы вложенных BoxView совместно с другими методами задания constraints, а также анимированное изменение параметров BoxView.
BoxView project on github
Хотя с появлением SwiftUI актуальность autolayout быстро уменьшается, пока этот механизм все еще активно используется, и библиотека может быть полезна для тех, кто создает (или меняет) UI непосредственно в коде.
У такого способа построения интерфейса есть ряд недостатков, которые ограничивают его применение:
- Очень неудобно организовано создание NSLayoutConstraint элементов.
- Плохая наглядность — посмотрев на код трудно понять как будет выглядеть UI.
- Большое количество рутинного кода. Для размещения каждой view требуется создание в среднем около 3 constraints, т.е. три строки однотипного кода.
- Трудоемкость создания динамически изменяемых интерфейсов: требуется сохранять constraints в отдельных переменных, чтобы затем можно было их менять, а также часто создавать избыточные constraints и «выключать» ненужные.
Первая проблема достаточно легко решается обертыванием стандартных методов создания constraints во что нибудь более гуманное. И это уже отлично реализовано, например, в SnapKit, TinyConstraints и других подобных библиотеках.
Но все равно приходится писать достаточно много однотипного кода, и остаются проблемы с наглядностью и динамическим изменениями лейаута. Эти проблемы изящно решает UIStackView, но к сожалению, в UIStackView очень ограничена настройка расположения отдельных элементов. Поэтому возникла идея контейнерной UIView, управляющей лейаутом стека своих subview, но с возможностью индивидуальной настройки расположения каждой subview.
Именно этот подход лежит в основе BoxView, и он оказался очень эффективным. BoxView позволяет почти полностью исключить ручное создание constraints, практически весь пользовательский интерфейс формируется в виде системы вложенных BoxView. В итоге код стал намного короче и нагляднее, выигрыш особенно ощутим для динамических UI.
BoxView во многом похожий на стандартный UIStackView, но для размещения subview он использует другие правила: в нем можно устанавливать отступы и размеры для каждой subview индивидуально. Для создания лейаута BoxView использует массив элементов типа BoxItem, который содержит все view которые надо отобразить, и информацию о том как их расположить. И для этого совсем не требуется много кода — большая часть параметров лейаута берется по умолчанию, а явно указываются только нужные значения.
Существенное свойство BoxView в том, что он создает только указанные constraints для добавленных subview, и ничего более. Поэтому его можно использовать без каких либо ограничений совместно с любыми другими библиотеками и методами лейаута.
В качестве примера рассмотрим создание простой логин формы с помощью BoxView (Полный код примера с пошаговым описанием доступен в проекте BoxViewExample на github).
Для создания такого лейаута на BoxView достаточно нескольких строк кода:
nameBoxView.items = [nameImageView.boxed.centerY(), nameField.boxed]
passwordBoxView.items = [passwordImageView.boxed.centerY(), passwordField.boxed]
boxView.insets = .all(16.0)
boxView.spacing = 20.0
boxView.items = [
titleLabel.boxed.centerX(padding: 30.0).bottom(20.0),
nameBoxView.boxed,
passwordBoxView.boxed,
forgotButton.boxed.left(>=0.0),
loginButton.boxed.top(30.0).left(50.0).right(50.0),
]
Элемент BoxItem создается из любой UIView с помощью переменной boxed, после этого ему можно задать отступы с 4 сторон, выравнивание, а также абсолютные или относительные размеры.
Любые элементы лейаута можно свободно добавлять и удалять (в том числе с анимацией), не затрагивая размещения остальных. В качестве примера добавим проверку на пустые поля ввода и в случае ошибки будем показывать сообщение непосредственно под пустым полем:
И хотя сообщение должно «встроиться» в существующий лейаут, для этого даже не потребуется менять имеющийся код!
func showErrorForField(_ field: UITextField) {
errorLabel.frame = field.convert(field.bounds, to: boxView)
let item = errorLabel.boxed.top(-boxView.spacing).left(errorLabel.frame.minX - boxView.insets.left)
boxView.insertItem(item, after: field.superview, z: .back)
boxView.animateChangesWithDurations(0.3)
}
@objc func onClickButton(sender: UIButton) {
for field in [nameField, passwordField] {
if field.text?.isEmpty ?? true {
showErrorForField(field)
return
}
}
// ok, can proceed with login
}
@objc func onChangeTextField(sender: UITextField) {
errorLabel.removeFromSuperview()
boxView.animateChangesWithDurations(0.3)
}
BoxView поддерживает весь инструментарий autolayout: расcтояния между элементами, абсолютные и относительные размеры, приоритеты, поддержку RTL языков. Помимо UIView в качестве элементов лейаута можно также использовать невидимые объекты — UILayoutGuides. Можно использовать также flex layout. Разумеется сама схема лейаута, в виде системы вложенных стеков UIView, не покрывает на 100% все мыслимые варианты относительного расположения элементов, но этого и не требуется. Она как раз хорошо походит для подавляющего большинство типичных пользовательских интерфейсов, а для более экзотических случаев, всегда можно добавить соответствующие дополнительные constraints любым другим способом. Несколько утилитных методов, например, для создания aspect ratio constraints также включены в библиотеку.
Еще один небольшой пример доступный на github (~100 строк кода!) иллюстрирует использование системы вложенных BoxView совместно с другими методами задания constraints, а также анимированное изменение параметров BoxView.
BoxView project on github
classx
а как на счет производительности? сравнение с SnapKit, TinyConstraints.
работает на tvOS?
anonymous Автор
Производительность BoxView это фактически производительность самого autolayout, поскольку BoxView просто создает NSLayoutConstraint, используя для этого обычные методы. Это же касается и SnapKit, TinyConstraints, Cartography и других аналогичных библиотек — все они имеют практически одинаковую производительность. У самого autolayout с производительностью не очень — она примерно на порядок меньше чем при ручной установке фреймов или в библиотеках типа LayoutKit, которые работают с фреймами напрямую. Но для построения пользовательских интерфейсов даже достаточно сложных и анимированных ее обычно вполне достаточно.
Да, BoxView работает на tvOS (правда проверялось только на симуляторе, если у кого-то есть возможность проверить на устройстве — буду рад такой информации)