DeclarativeLayoutKit
A simple and small framework of tools for fast and declarative UI layout.
Note: it is powered by SnapKit!
Advantages | |
---|---|
Fast view configuration using property chains | |
Easily layout using simplest DSL based and mixed with SnapKit DSL | |
Extra feature β stacking views layout like UIStackView, but more flexible | |
Reusable and mixable view styling | |
Fully SCALABLE! |
Overview
- Requirements
- Usage
- Why should I choose DeclarativeLayoutKit over alternative?
- Installation
- Credits
- License
Requirements
- iOS 10.0+
- Xcode 10.0+
- Swift 5.0+
Usage
π Property chaining
Absolutely all mutable properties are represented in the function using code generation powered by Sourcery.
let myLabel = UILabel()
.numberOfLines(0)
.text("Hello buddy")
.backgroundColor(.blue)
.isHighlighted(true)
.borderWidth(1)
.borderColor(.cyan)
Currently, property chaining is supported in the following types:
UIView
, UIControl
, UILabel
, UIImageView
, UIScrollView
, UITextView
, UITableView
, UICollectionView
, UITextField
, UIButton
, UISlider
, UISwitch
, UIStackView
.
You can also easily generate functions for other types β see how
π And some extra syntactic sugar:
Assignable to variable:
class ViewController: UIViewController {
weak var myLabel: UILabel!
override func loadView() {
...
view.addSubview(
UILabel()
.numberOfLines(0)
.text("Voila")
// sets a reference of object that calls function(in this case, created UILabel instance) to passed variable
.assign(to: &myLabel)
...
)
}
}
Closure based actions and gestures:
UIControl()
.addAction(for: .valueChanged, { print("value changed") })
UIButton()
.title("Tap me")
.onTap({ print("didTap") })
UIView()
.onTapGesture({ print("Kek") })
.onLongTapGesture({ print("Cheburek") })
// β οΈ Don't forget about ARC when use some parent view in action closure, to prevent retain cycle
Simple UIStackView helpers:
HStackView
-UIStackView().axis(.horizontal)
VStackView
-UIStackView().axis(.vertical)
Declarative anchor builder
You can set constraints using the same anchor-style. The return type will be AnchorLayoutBuilder
β a simple container that stores declared anchor's constants. To apply them, just call build()
function.
let myLabel = UILabel()
.numberOfLines(0) // -> UIView
...
.heightAnchor(0) // -> AnchorLayoutBuilder (same below)
.topAnchor(16.from(anotherView.snp.bottom))
.leftAnchor(24)
.rightAnchor(24.orLess.pririty(750))
.build() // -> UIView (with applyed constraints)
The frameworks allows two ways to set constants:
let myLabel = UILabel()
.numberOfLines(0)
.layout({
$0.height.equalTo(0)
$0.top.equalTo(anotherView.snp.bottom).inset(16)
$0.left.equalToSuperview().inset(14)
$0.right.lessThanOrEqualToSuperview().inset(24)
})
.build()
- own DSL powered by
AnchorLayoutBuilderConstraint
It has the following template:
inset.from(SnapKit.ConstraintPriorityTarget).priority(UILayoutPriority)
If specify only inset
it will be applied to the superview
:
myView.horizontalAnchor(16).topAnchor(0).bottomAnchor(44)
If you want to change Ρomparison type (less/greater or equal) add .orLess
or .orGreater
suffix after inset
myView.bottomAnchor(44.orLess).rightAnchor(8.orGreater.from(secondView))
β οΈ Default priority ofAnchorLayoutBuilderConstraint
is 999! It was decided to do so to ensure that the constraints in the case when its could not be applyed, in the future, when updating layout, constraints are automatically re-activated
Full list of anchors functions:
width/height/left/right/top/bottom/centerX/centerYAnchor
- completely equal to NSLayoutConstraint
anchors
sizeAnchor(CGSize)
== widthAcnhor
+ heightAnchor
aspectRatioAnchor(multiplier:)
== height / width
horizontalAnchor
== leftAnchor
+ rightAnchor
verticalAnchor
== topAnchor
+ bottomAnchor
centerAnchor
== centerXAnchor
+ centerYAnchor
edgeAnchors(insets: UIEdgeInsets, to target: SnapKit.ConstraintRelatableTarget?)
- stretches all the edges to target
. The default target
is nil
, which is equivalent to superview
.
View/Builder Composition
You can also add views
builders to the superview
builder like this:
weak var avatarView: UIImageView!
...
let profileView = UIView()
.backgroundColor(.gray)
.heightAnchor(100)
.add({
UIImageView()
.assign(to: &avatarView)
.contentMode(.scaleAspectFit)
.sizeAnchor(CGSize(width: 40, height: 40))
.leftAnchor(16)
.verticalAnchor(0)
UILabel()
.numberOfLines(2)
.rightAnchor(0)
.leftAnchor(8.from(avatarView).priority(.required))
})
// current view is build first, and then it's subviews
.build()
Or using a convenient initializer
let profileView = UIView({
UIImageView()
.assign(to: &avatarView)
.contentMode(.scaleAspectFit)
.sizeAnchor(CGSize(width: 40, height: 40))
.leftAnchor(16)
.verticalAnchor(0)
UILabel()
.numberOfLines(2)
.rightAnchor(0)
.leftAnchor(8.from(avatarView).priority(.required))
})
.backgroundColor(.gray)
.heightAnchor(100)
// current view is build first, and then it's subviews
.build()
Note: if you add builders to a UIView
(i.e. don't use anchor-chaining
before call add(...)
), building will occur immediately after adding in superview
. In other words, the return type of the build()
function will be Self
(i.e. UIView
)
let profileView = UIView()
.backgroundColor(.gray)
.add({
UIImageView()
.assign(to: &avatarView)
.contentMode(.scaleAspectFit)
.sizeAnchor(CGSize(width: 40, height: 40))
.leftAnchor(16)
.verticalAnchor(0)
UILabel()
.numberOfLines(2)
.rightAnchor(0)
.leftAnchor(8.from(avatarView).priority(.required))
}) // -> UIView (with already added subviews)
// and you can continue chainable-configuration (for example by specifying own anchors)
.heightAnchor(100) // -> AnchorLayoutBuilder
.build() // -> UIView
π Bonus! Declarative stack builder
Sometimes UIStackView
features are not enough for more flexible stacking of views
with an individual distribution.
The StackingLayoutBuilder
will help you solve this problem!
let profileView = HStack {
UIImageView()
.assign(to: &avatarView)
.contentMode(.scaleAspectFit)
.sizeAnchor(CGSize(width: 40, height: 40))
.leftSpace(12) // π
.verticalAlignment(.center) // π
UILabel()
.numberOfLines(2)
.leftSpace(8) // π
.rightSpace(0) // π
}
.backgroundColor(.gray)
HStack
and VStack
is a function that collects views
(or AnchorLayoutBuilder
) and stack them to each other using AutoLayout Constants and return resulting UIView
.
You can specify:
left/rightSpace
(in HStack
), top/bottomSpace
(in VStack
) - before and after space of arranged view.
verticalAlignment
(in HStack
), horizontalAlignment
(in VStack
) - arranged view distribution in transverse axis.
Alignment
type can be:
-
.center
- centered on transverse axis. -
.fill(sideInset: AnchorLayoutBuilderConstraint = 0)
- stretched along transverse axis. -
HStack
(similarly inVStack
):.custom(top: bottom:)
- custom top and bottom inset (i.e.AnchorLayoutBuilderConstraint
).bottom(...)
- set only bottom inset..top(..)
- - set only top inset.
You can continue to use anchor-chaining
to add additional constraints, but only before specifying .space()
and .alignment()
attributes!
π Declarative View Styling
You no longer need to create many style-different subclasses of your view and dive into a complex inheritance hierarchy.
DeclarativeLayoutKit offers a simple closure-based solution for defining styles with the ability to combine them:
UIButton()
.title("Tap me")
.set(style: .touchHiglighting(color: .blue), .primaryRounded())
.onTap({ ... })
Just create new ViewStyle<Target>
in ViewStyle
extension like static factory:
extension ViewStyle {
/// Generic definition is required to be able to support view subclass types
static func primaryRounded<T: UIView>() -> ViewStyle<T> {
ViewStyle<T>({ view in
view.backgroundColor(.systemBlue)
.cornerRadius(8)
.borderWidth(4)
.shadowColor(.darkGray)
.shadowOpacity(1)
.shadowRadius(5)
})
}
static func touchHiglighting<T: UIButton>(color: UIColor) -> ViewStyle<T> {
ViewStyle<T> { button in ... }
}
}
About ViewStyle
The ViewStyle
is just a wrapper over closure. There's nothing ingenious about it
public final class ViewStyle<Target: ViewStyleCompatible> {
private let handler: (Target) -> ()
public init(_ handler: @escaping (Target) -> ()) {
self.handler = handler
}
func apply(into target: Target) {
handler(target)
}
}
π₯ All together
And finally, a full-fledged example of using the framework for layout of restaurant previews
Layout:
Source:
π§© How to extend functionality?
Add property chaining to another type:
- First way - write type extension with function that return self:
extension MyCustomView {
func myProperty(_ value: ValueType) -> Self {
self.myProperty = value
return self
}
}
- Second way - using Sourcery apply Chainable template with your custom view (see tutorial).
AnchorLayoutBuilder
:
Extend Just add extension to AnchorLayoutBuilderConvertible
type and using SnapKit DSL
write your own helper:
extension AnchorLayoutBuilderConvertible {
func leftTopAnchor(_ inset: CGFloat) -> AnchorLayoutBuilder {
self.layout({ $0.left.top.equalToSuperview().inset(inset) })
}
}
Or use .set(constraint: AnchorLayoutBuilderConstraint)
to SnapKit Anchor if you want to use your own DSL:
extension AnchorLayoutBuilderConvertible {
func leftTopAnchor(_ constraint: AnchorLayoutBuilderConstraint) -> AnchorLayoutBuilder {
self.layout({ $0.left.top.set(constraint: constraint) })
}
}
Extend DSL:
The AnchorLayoutBuilderConstraint
is protocol 4 properties:
target
inset
(optional)comparisonType
(equal, less, greater)priority
You can write some extension to it for adding another syntactic sugar and modify this constraint using MutableAnchorLayoutBuilderConstraint
implementation.
Why should i choose this framework?
There are already quite a few frameworks for declarative layout like SwiftUI, but DeclarativeLayutKit stands out from them:
-
Small codebase
There is no huge number of objects, I'm not "reinventing the wheel". This is not a new layout system - just a set of helpers:AnchorLayoutBuilder
,StackingLayoutBuilder
,ViewStyle
. ThisREADME
deccription is all you need to know, no redundant documentation needed.
π And you can easily integrate the framework into project and combine it with old/existing layout code. -
Extandable
The framework is simple and easy to expand. Just applyBuilder
andFactory
pattern to add new functionality. -
Separately
The framework solves 3 tasks:- property-chaining
- layouting
- styling
And you can use a specific, desired set of features. See more in installation chapter
Installation
CocoaPods
# Podfile
use_frameworks!
target 'YOUR_TARGET_NAME' do
pod 'DeclarativeLayoutKit'
# if you only want property-chaining feature:
# pod 'DeclarativeLayoutKit/Chaining'
# if you only want anchor-layouting feature:
# pod 'DeclarativeLayoutKit/Layouting'
# if you only want view styling feature:
# pod 'DeclarativeLayoutKit/Styling'
end
Replace YOUR_TARGET_NAME
and then, in the Podfile
directory, type:
$ pod install
Swift Package Manager
Create a Package.swift
file.
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "YOUR_PROJECT_NAME",
dependencies: [
.package(url: "https://github.com/Ernest0-Production/DeclarativeLayoutKit.git", from: "1.0.0")
],
targets: [
.target(name: "YOUR_TARGET_NAME", dependencies: ["DeclarativeLayoutKit"])
// if you only want property-chaining feature:
// dependencies: ["DeclarativeLayoutKit/Chaining"])
// if you only want anchor-layouting feature:
// dependencies: ["DeclarativeLayoutKit/Layouting"])
// if you only want view styling feature:
// dependencies: ["DeclarativeLayoutKit/Styling])
]
)
Credits
- Telegram: @Ernest0N
License
DeclarativeLayoutKit is released under the MIT license. See LICENSE for details.
TODO:
- replaceable layout system (NSLayutConstraints, SnapKit, FrameBasedLayout)
- placeholderable?
- loadable?