Хочу поделиться библиотекой для эффективного построения пользовательского интерфейса iOS приложений на основе autolayout.

Хотя с появлением 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).

image

Для создания такого лейаута на 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 сторон, выравнивание, а также абсолютные или относительные размеры.

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

image

И хотя сообщение должно «встроиться» в существующий лейаут, для этого даже не потребуется менять имеющийся код!

    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.

image

BoxView project on github