- PVSM.RU - https://www.pvsm.ru -
Привет!
Хочу представить мою последнюю open-source разработку — CGLayout
— вторая система разметки в iOS после Autolayout, основанная на ограничениях.
"Очередная система автолайаута… Зачем? Для чего?" — наверняка подумали вы.
Действительно iOS сообществом создано уже немало layout-библиотек, но ни одна так и не стала по-настоящему массовой альтернативой ручному layout`у, не говоря уже про Autolayout.
CGLayout
работает с абстрактными сущностями, что позволяет одновременно использовать UIView, CALayer и not rendered
объекты для построения разметки. Также имеет единое координатное пространство, что позволяет строить зависимости между элементами, находящимися на разных уровнях иерархии. Умеет работать в background потоке, легко кешируется, легко расширяется и многое-многое другое.
CGLayout
функциональный продукт, у которого есть хорошие перспективы развиться в большой проект.
Но изначально цель была банальна, как и обычно, просто упростить себе жизнь.
Всем иногда приходится писать ручной layout, либо из-за плохой производительности Autolayout, либо из-за сложной логики. Поэтому постоянно писались какие-то расширения (а-ля setFrameThatFits и т.д.)
В процессе реализации таких расширений возникают идеи более сложного порядка, но из-за отсутствия времени, как обычно, это все остается в рамках записи в Trello и висит там вечность.
Но когда ты наконец добрался до реализации и не видно горизонта, тебя затягивает, и остановиться уже нереально. Все-таки я надеюсь время было потрачено не зря и мое решение облегчит кому-то жизнь, если не функциональностью фреймворка, то примером кода.
Помимо рассказа о своем решении, я попробую проанализировать и сравнить другие фреймворки, поэтому думаю скучно не будет.
Требования | FlexLayout | ASDK (Texture) | LayoutKit | Autolayout | CGLayout |
---|---|---|---|---|---|
Производительность | + | + | + | - | + |
Кешируемость | + | + | + | +- | + |
Мультипоточность | - | + | + | - | + |
Cross-hierarchy layout | - | - | - | + | + |
Поддержка CALayer и 'not rendered' вью | - | + | - | - | + |
Расширяемость | - | + | + | - | + |
Тестируемость | + | + | + | + | + |
Декларативный | + | + | + | + | + |
Какие-то показатели могут быть субъективны, т.к. я не использовал эти фреймворки в production. Если я ошибся, поправьте меня пожалуйста.
Для тестирования использовался LayoutFrameworkBenchmark [1].
AsyncDisplayKit не добавлен в график в силу того, что он не был включен разработчиком бенчмарка, да и ASDK осуществляет layout в бэкграунде, что не совсем честно для измерений производительности. В качестве альтернативы можно посмотреть приложение Pinterest. Производительность там действительно впечатляющая.
Про многие фреймворки уже есть много информации, я лишь поделюсь своим мнением. Я буду говорить в основном о негативных моментах, потому что они бросаются в глаза сразу, а плюсы описаны на страницах фреймворков.
Github [2] 25 issues
Не очень гибкий, требует написания большого количества кода, и достаточно большого включения программиста в реализацию. Не нашел информации об использовании LayoutKit'а в каких-то других приложениях, кроме самого LinkedIn.
И все-таки есть мнение, что LayoutKit не достиг своей цели, лента в приложении LinkedIn все равно тормозит.
Особенности:
Github [3] 65 issues
AppSight [4]
Предоставляет возможность заниматься только layout'ом. Никаких других плюшек, фишек нет.
Особенности:
Github [5] 200 issues
AppSight [6]
В Facebook пошли путем создания потокобезопасной абстракции более высокого уровня. Что вызвало необходимость реализовывать весь стек инструментов UIKit. Тяжелая библиотека, если вы решили ее использовать, отказаться потом от нее будет невозможно. Но все-таки это пока самое грамотное и развитое open-source решение.
Особенности:
not rendered
объекты.Текущие ограничения:
layer
свойство ведет к неопределенному поведению, так как frame
у UIView изменяется неявно и побочные действия (такие как drawRect, layoutSubviews) не вызываются. При этом layer
UIView спокойно можно использовать как ограничение для другого layer'а.bounds
в супервью и наличии ограничений, основанных на супервью, может приводить к неожидаемому результату.Что пока не реализовано:
CGLayout
построен на современных принципах языка Swift.
Реализация управления разметкой в CGLayout базируется на трех базовых протоколах: RectBasedLayout
, RectBasedConstraint
, LayoutItem
.
Все сущности имплементирующие LayoutItem
я буду называть layout-элементами, все остальные сущности просто layout-сущностями.
public protocol RectBasedLayout {
func layout(rect: inout CGRect, in source: CGRect)
}
RectBasedLayout
— декларирует поведение для изменения разметки и определяет для этого один метод, с возможностью ориентирования относительно доступного пространства.
Структура Layout
, имплементирующая протокол RectBasedLayout
, определяет полную и достаточную разметку для layout-элемента, т.е. позиционирование и размеры.
Соответственно, Layout
разделяется на два элемента выравнивание Layout.Alignment
и заполнение Layout.Filling
. Они в свою очередь состоят из горизонтального и вертикального лайаута. Все составные элементы реализуют RectBasedLayout
. Что позволяет использовать элементы лайаута разного уровня сложности для реализации разметки. Все лайаут сущности легко могут быть расширены вашими имплементациями.
Все ограничения реализуют RectBasedConstraint
. Если сущности RectBasedLayout
определяют разметку в доступном пространстве, то сущности RectBasedConstraint
это доступное пространство определяют.
public protocol RectBasedConstraint {
func constrain(sourceRect: inout CGRect, by rect: CGRect)
}
LayoutAnchor
содержит конкретные ограничители (сторона, размер и т.д.), имеющие абстрагированное от окружения поведение.
В данный момент реализованы основные ограничители.
public protocol LayoutConstraintProtocol: RectBasedConstraint {
var isIndependent: Bool { get }
func layoutItem(is object: AnyObject) -> Bool
func constrainRect(for currentSpace: CGRect, in coordinateSpace: LayoutItem) -> CGRect
}
Определяют зависимость от layout-элемента или контента (текст, картинка и т.д.). Являются самодостаточными ограничениями, которые содержат всю информацию об источнике ограничения и применяемых ограничителях.
LayoutConstraint
— ограничение, связанное с layout-элементом с определенным набором ограничителей.
AdjustLayoutConstraint
— ограничение, связанное с layout-элементом, содержит size-based ограничители. Доступен для layout-элементов, поддерживающих AdjustableLayoutItem
протокол.
public protocol LayoutItem: class, LayoutCoordinateSpace {
var frame: CGRect { get set }
var bounds: CGRect { get set }
weak var superItem: LayoutItem? { get }
}
Его реализуют такие классы как UIView, CALayer, а также not rendered
классы. Также вы можете реализовать другие классы, например stack view.
Во фреймворке есть реализация LayoutGuide. Это аналог UILayoutGuide из UIKit, но с возможностью фабрики layout-элементов. Что позволяет использовать LayoutGuide в качестве плейсхолдера, что довольно актуально в свете последних дизайн решений. В частности для этих целей создан класс ViewPlaceholder. Он реализует такой же паттерн загрузки вью как и UIViewController. Поэтому работа с ним будет очень знакомой.
Для элементов, которые могут рассчитать свой размер задекларирован протокол:
public protocol AdjustableLayoutItem: LayoutItem {
func sizeThatFits(_ size: CGSize) -> CGSize
}
По умолчанию его реализуют только UIView.
public protocol LayoutCoordinateSpace {
func convert(point: CGPoint, to item: LayoutItem) -> CGPoint
func convert(point: CGPoint, from item: LayoutItem) -> CGPoint
func convert(rect: CGRect, to item: LayoutItem) -> CGRect
func convert(rect: CGRect, from item: LayoutItem) -> CGRect
var bounds: CGRect { get }
var frame: CGRect { get }
}
Система лайаута имеет объединенную координатную систему представленную в виде протокола LayoutCoordinateSpace
.
Она создаёт единый интерфейс для всех layout-элементов, при этом используя основные реализации каждого из типов (UIView, CALayer, UICoordinateSpace + собственная реализация для кросс-конвертации).
public protocol LayoutBlockProtocol {
var currentSnapshot: LayoutSnapshotProtocol { get }
func layout()
func snapshot(for sourceRect: CGRect) -> LayoutSnapshotProtocol
func apply(snapshot: LayoutSnapshotProtocol)
}
Layout-блок является законченной и самостоятельной единицей макета. Он определяет методы для выполнения разметки, получения/применения snapshot`а.
LayoutBlock
инкапсулирует layout-элемент, его основной лайаут и ограничения, реализующие LayoutConstraintProtocol.
Процесс актуализации разметки начинается с определения доступного пространства с помощью ограничений. Следует учитывать, что система пока никак решает проблем с конфликтными ограничениями и никак их не приоритезирует, поэтому следует внимательно подходить к применению ограничений. Так в общем случае, ограничения основанные на размере (AdjustLayoutConstraint) следует ставить после ограничений, основанных на позиционировании. В качестве исходного пространства берется пространство супервью (bounds). Каждое ограничение изменяет доступное пространство (обрезает, смещает, растягивает и т.д.). После того как ограничения отработали, полученное пространство передается в Layout
, где и рассчитывается актуальная разметка для элемента.
LayoutScheme
— блок, который объединяет другие лайаут блоки и определяет корректную последовательность для выполнения разметки.
public protocol LayoutSnapshotProtocol {
var snapshotFrame: CGRect { get }
var childSnapshots: [LayoutSnapshotProtocol] { get }
}
LayoutSnapshot
— снимок представленный в виде набора фреймов, сохраняя иерархию layout-элементов.
Все расширяемые элементы реализуют протокол Extended.
public protocol Extended {
associatedtype Conformed
static func build(_ base: Conformed) -> Self
}
Таким образом, при расширении функционала вы можете использовать уже определенный в CGLayout
тип, для построения строго типизированного интерфейса.
CGLayout почти не отличается от ручного лайаута в смысле построения последовательности актуализации фреймов. Реализуя разметку в CGLayout программист обязан помнить, о том, что все ограничения до начала работы должны оперировать актуальными фреймами, иначе результат будет неожидаемым.
let leftLimit = LayoutAnchor.Left.limit(on: .outer)
let topLimit = LayoutAnchor.Top.limit(on: .inner)
let heightEqual = LayoutAnchor.Size.height()
...
let layoutScheme = LayoutScheme(blocks: [
distanceLabel.layoutBlock(with: Layout(x: .center(), y: .bottom(50),
width: .fixed(70), height: .fixed(30))),
separator1Layer.layoutBlock(with: Layout(alignment: separator1Align, filling: separatorSize),
constraints: [distanceLabel.layoutConstraint(for: [leftLimit, topLimit, heightEqual])])
...
])
...
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
layoutScheme.layout()
}
В этом примере сепаратор использует ограничения по distanceLabel
, после того, как определено положение этого лейбла.
Autolayout пока остаётся основным инструментом лайаута в силу своей стабильности, хорошего API и мощной поддержкой. Но сторонние решения могут помочь решить частные проблемы, в силу своей узкой направленности или гибкости.
CGLayout
имеет не совсем привычную логику описания процесса layout'а, поэтому требует привыкания.
Тут еще много работы, но это вопрос времени, при этом уже сейчас видно, что он имеет ряд преимуществ, которые должны ему позволить занять свою нишу в области подобных систем. Работа фреймворка ещё не тестировалось в production, и у вас есть возможность попробовать это сделать. Фреймворк покрыт тестами, поэтому больших проблем возникнуть не должно.
Надеюсь на ваше активное участие в дальнейшей разработке фреймворка.
И в конце хотелось бы поинтересоваться у хабра-юзеров:
Какие требования предъявляете вы к layout-системам?
Что вам больше всего не нравится делать при построении верстки?
Автор: Denis Koryttsev
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/uikit/264279
Ссылки в тексте:
[1] LayoutFrameworkBenchmark: https://github.com/lucdion/LayoutFrameworkBenchmark
[2] Github: https://github.com/linkedin/LayoutKit
[3] Github: https://github.com/lucdion/FlexLayout
[4] AppSight: https://www.appsight.io/sdk/574513
[5] Github: https://github.com/texturegroup/texture
[6] AppSight: https://www.appsight.io/sdk/asyncdisplaykit
[7] Github репозиторий: https://github.com/k-o-d-e-n/CGLayout
[8] Источник: https://habrahabr.ru/post/338540/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.