Draftsman is a DSL framework for Swift focused on builder pattern If you are still using version 2.3.x, Separated README is available here. If you are still using Swift 5.1, please use 1.1.x version. Separated README is available here.
To run the example project, clone the repo, and run pod install
from the Example directory first.
Draftsman is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'Draftsman', '~> 3.1.0'
Add as your target dependency in Package.swift
dependencies: [
.package(url: "https://github.com/hainayanda/Draftsman.git", .upToNextMajor(from: "3.1.0"))
]
Use it in your target as a Draftsman
.target(
name: "MyModule",
dependencies: ["Draftsman"]
)
Nayanda Haberty, [email protected]
Draftsman is available under the MIT license. See the LICENSE file for more info.
Draftsman is the NSLayoutConstraints
and UIView
hierarchy builder. Draftsman uses a new resultBuilder from Swift that makes the Declarative approach possible.
Creating constraints is very easy. All you need to do is call drf
to get the LayoutDraft
object:
myView.drf
.left.equal(to: otherView.drf.right)
.right.equal(with: .parent).offset(by: 16)
.top.lessThan(with: .safeArea).offSet(8)
.bottom.moreThan(with: .top(of: .keyboard))
.apply()
there are two methods to end planning constraints which can be called from both any UIView
or UIViewController
:
func apply() -> [NSLayoutConstraint]
func build() -> [NSLayoutConstraint]
the difference between the two is apply
will activate the constraints but build
will only create constraints without activating them. Apply return value is discardable so it's optional for you to use the created NSLayoutConstraint
or not.
You could always create a UIViewController
or UIView
and implement the Planned
protocol, and call applyPlan()
whenever you want the viewPlan
to be applied:
import Draftsman
class MyViewController: UIViewController, Planned {
var models: [MyModel] = []
@LayoutPlan
var viewPlan: ViewPlan {
VStacked(spacing: 32) {
if models.isEmpty {
MyView()
MyOtherView()
SomeOtherView()
} else {
for model in models {
MyModeledView(model)
}
}
}
.centered()
.matchSafeAreaH().offset(by: 16)
.vertical.moreThan(with: .safeArea).offset(by: 16)
}
override func viewDidLoad() {
super.viewDidLoad()
applyPlan()
}
}
ViewPlan
can always be composed to make the code cleaner:
import Draftsman
class MyViewController: UIViewController, Planned {
var models: [MyModel] = []
@LayoutPlan
var viewPlan: ViewPlan {
VStacked(spacing: 32) {
stackPlan
}
.centered()
.matchSafeAreaH().offset(by: 16)
.vertical.moreThan(with: .safeArea).offset(by: 16)
}
@LayoutPlan
var stackPlan: ViewPlan {
if models.isEmpty {
emptyStackPlan
} else {
modeledStackPlan(for: models)
}
}
@LayoutPlan
var emptyStackPlan: ViewPlan {
MyView()
MyOtherView()
SomeOtherView()
}
@LayoutPlan
func modeledStackPlan(for models: [MyModel]) -> ViewPlan {
for model in models {
MyModeledView(model)
}
}
override func viewDidLoad() {
super.viewDidLoad()
applyPlan()
}
}
You can create a view hierarchy while creating constraints by using the draftContent
or drf.insert
method and insert
method for the subview draft (draftStackedContent
or drf.insertStacked
and insertStacked
if its arranged subviews in UIStackView
). Don't forget to call apply()
or build()
, Both will rearrange the view hierarchy but only apply()
will activate the constraints created.
view.draftContent {
UIView().drf
.center.equal(to: .parent)
.horizontal.equal(to: .safeArea)
.vertical.moreThan(with: .safeArea)
.insert {
myView.drf
.edges(.equal, to: .parent)
}
}.apply()
The hierarchy of Views is just like how the closure is declared in your code. The above code actually will do the following instruction sequentially:
view
create and insert a new UIView()
UIView
then will create constraintsUIView
then will insert myView
myView
then will create constraintsSo if the hierarchy is written in pseudo hierarchy style, it should be similar to this:
view
|____new UIView
| |____myView
the compatible type to be passed in the closure are:
UIView
UIViewController
If you pass UIViewController
, it will be automatically added the UIViewController
view as a child and put the UIViewController
as a child of its current UIViewController
.
You could insert components as much as you need, it will fit all the Views just like how you write them.
You can build your view using Builder library built-in in the Draftsman by calling the builder
property and get back to Draftsman by calling drf
again:
myView.drf
.center.equal(to: .parent)
.builder.backgroundColor(.black)
.drf.bottom.moreThan(to: .safeArea)
Positioning a View is easy. You just need to declare which anchor should have relation to others:
myView.drf
.top.equal(to: other.drf.top)
.right.moreThan(to: other.drf.right).offset(by: 8)
.bottom.lessThan(to: other.drf.bottom).offset(by: 8).priority(.required)
.left.equal(to: other.leftAnchor)
.centerX.moreThan(to: other.centerXAnchor).inset(by: 8)
.centerY.lessThan(to: other.centerYAnchor).inset(by: 8).identifier("centerY")
basic position anchors available from Draftsman are:
All are available for both UIView
and UILayoutGuide
This can be used to create a constraint using one of these three methods:
Those methods can accept basic NSLayoutAnchor
from UIKit
or use Anchor
from Draftsman
as long it's in the same Axis.
To add a constant, use one of the offset(by:)
or inset(by:)
methods. offsetis the spacing going to the outer part of the anchor and
inset` are spacing going to the inner part of the anchor:
For center anchor, offset and inset can be described in this picture:
You can then add priority or/and an identifier for the constraints created.
Dimensioning a View is easy. You just need to declare which anchor should have relation to others or constant:
myView.drf
.height.equal(to: other.drf.width)
.width.moreThan(to: other.drf.height).added(by: 8)
.height.lessThan(to: anyOther.heightAnchor).substracted(by: 8).priority(.required)
.width.equal(to: anyOther.widthAnchor).multiplied(by: 0.75).identifier("width")
basic dimension anchors available from Draftsman are:
All are available for both UIView
and UILayoutGuide
This can be used to create a constraint using one of these three methods:
Those methods can accept basic NSLayoutDimension
from UIKit
or use dimension Anchor
from Draftsman
.
To add a constant, use one of the added(by:)
, substracted(by:)
, or multiplied(by: )
methods.
You can then add priority or/and an identifier for the constraints created.
Dimensioning can be achieved using constant too:
myView.drf
.height.equal(to: 32)
.width.moreThan(to: 64)
.width.lessThan(to: 128).priority(.required).identifier("width")
Very similar except it accepts CGFloat
Creating constraints using multiple anchors is very easy, you can always combine two or more anchors and use them to create multiple constraints at once:
myView.drf
.top.left.equal(to: other.drf.top.left)
.bottom.left.right.moreThan(to: anyOther.drf.top.left.right)
It will be similar to single anchors, but you can only be passed Draftsman Anchor
with the same Axis combination:
There are some shortcuts for anchor combinations:
Example:
myView.drf
.vertical.equal(to: other.drf.vertical)
.bottom.horizontal.moreThan(to: anyOther.drf.top.horizontal)
Sizing with size or width.height can be achieved by using CGSize
too if needed:
myView.drf
.size.equal(to: CGSize(sides: 30))
for offsets and insets, CGFloat
is compatible with all. But if you need to assign it explicitly for each edge, you can always be passing something else:
CGPoint
CGPoint
UIEdgeInsets
UIEdgeInsets
You can pass just UIView
or UILayoutGuide
instead of Anchor
explicitly and it will use the same anchor to make constraints:
myView.drf
.vertical.equal(to: otherView)
.bottom.horizontal.moreThan(to: view.safeAreaLayoutGuide)
In the example above, it will create equal constraints between myView
vertical anchors and otherView
vertical anchors, then it will create another with myView
bottom and view.safeAreaLayoutGuide
bottom.
Sometimes you don't want or even can't use anchor explicitly. In those cases, you can always use AnonymousLayout
:
myView.drf
.top.left.equal(with: .parent)
.bottom.moreThan(with: .safeArea).offset(by: 16)
.size.lessThan(with: .previous)
available AnonymousLayout
are:
It's the same as a regular anchor, but it will automatically get the same anchor for an anonymous view. If you want to explicitly get a different anchor of anonymous, then you can do something like this:
myView.drf
.top.equal(with: .top(of:.parent))
.bottom.moreThan(with: .bottom(of: .safeArea)).offset(by: 16)
.width.lessThan(with: .height(of: .previous))
available explicit anchors are:
There are several shortcuts for building layout constraints that can be accessed via drf
:
edges.equal(with: .parent)
edges.equal(with: .safeArea)
horizontal.equal(with: .parent)
vertical.equal(with: .parent)
horizontal.equal(with: .safeArea)
vertical.equal(with: .safeArea)
size.equal(with: .parent)
center.equal(with: .parent)
centerX.equal(with: .parent)
centerY.equal(with: .parent)
top.left.equal(with: .parent)
, or any other cornerwidth.equal(with: .height(of: .mySelf))
height.equal(with: .width(of: .mySelf))
size.equal(with: givenSize)
You can use SpacerView as a Spacer for UIStackView content:
UIScrollView().drf.insertStacked {
MyView()
SpacerView(12)
OtherView()
}
or leave the init empty if you want the spacer size to be dynamic:
UIScrollView().drf.insertStacked {
MyView()
SpacerView()
OtherView()
}
There are custom UIView
named ScrollableStackView
which is a UIStackView
inside UIScrollView
. You can use it if you need a UIStackView
that can be scrolled if the content is bigger than the container. It has 2 public init that can be used:
Other than that, it can be used like regular UIStackView
and regular UIScrollView
minus the capability to change its distribution, since it needed to make sure the view behaves as it should.
Some helpers can be used if you want your viewPlan
shorter and less explicit. This helper will accept LayoutPlan
closure so you don't need to access it via drf
but directly on its init
HStacked
and VStacked
are a shortcut to create vertical and horizontal UIStackView without creating it explicitly. It has 3 public init that can be used:
Example:
VStacked(distribution: .fillEqually) {
SomeView()
MyView()
OtherView()
}
.fillParent()
This will be equivalent with:
UIStackView(axis: .vertical, distribution: .fillEqually).drf
.fillParent()
.insertStacked {
SomeView()
MyView()
OtherView()
}
HScrollableStacked
and VScrollableStacked
are a shortcut to create vertical and horizontal ScrollableStackView
without creating it explicitly. It has 3 public init that can be used:
Example:
HScrollableStacked(alignment: .fill) {
SomeView()
MyView()
OtherView()
}
.fillParent()
This will be equivalent with:
ScrollableStackView(axis: .horizontal, alignment: .fill).drf
.fillParent()
.insertStacked {
SomeView()
MyView()
OtherView()
}
Margined
is a simple way to add a margin to any UIView
. Example:
Margined(by: 12) {
MyView()
}
.fillParent()
This will be equivalent with:
UIView().drf.builder
.backgroundColor(.clear)
.drf.fillParent()
.insert {
MyView().fillParent().offsetted(by: 12)
}
Draftsman Planned
protocol is the protocol that makes any UIView
or UIViewController
can have its predefined view plan and applied it using the applyPlan
method. The protocol is declared like this:
public protocol Planned: AnyObject {
var planIdentifier: ObjectIdentifier { get }
var appliedConstraints: [NSLayoutConstraint] { get }
var viewPlanApplied: Bool { get }
@LayoutPlan
var viewPlan: ViewPlan { get }
@discardableResult
func applyPlan() -> [NSLayoutConstraint]
}
The only thing you need to implement is the viewPlan
getter since everything will be implemented in extensions:
import Draftsman
class MyViewController: UIViewController, Planned {
@LayoutPlan
var viewPlan: ViewPlan {
VStacked(spacing: 32) {
MyView()
MyOtherView()
SomeOtherView()
}
.centered()
.matchSafeAreaH().offset(by: 16)
.vertical.moreThan(with: .safeArea).offset(by: 16)
}
override func viewDidLoad() {
super.viewDidLoad()
applyPlan()
}
}
Every time you call applyPlan
, it will always try to recreate the view to be the same as what was declared in viewPlan
.
There are some typealias with Planned
that you can use:
UIViewController & Planned
UIView & Planned
PlannedCell
is Planned
built specifically for a cell which declared like this:
public protocol PlannedCell: Planned {
@LayoutPlan
var contentViewPlan: ViewPlan { get }
}
The only thing you need to implement is the contentViewPlan
getter since everything will be implemented in extensions. It will skip contentView
and straight into its content:
class TableCell: UITableView, PlannedCell {
@LayoutPlan
var contentViewPlan: ViewPlan {
HStacked(margin: 12, spacing: 8) {
UIImageView(image: UIImage(named: "icon_test")).drf
.sized(CGSize(sides: 56))
VStacked(margin: 12, spacing: 4) {
UILabel(text: "title text")
UILabel(text: "subtitle text")
}
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
applyPlan()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
applyPlan()
}
}
Every time you call applyPlan
, it will always try to recreate the view to be the same as what was declared in viewPlan
.
There are some typealias with Planned
that you can use:
UITableViewCell & PlannedCell
UICollectionViewCell & PlannedCell
PlannedStack
is Planned
built specifically for a cell which declared like this:
public protocol PlannedStack: Planned {
@LayoutPlan
var stackViewPlan: ViewPlan { get }
}
The only thing you need to implement is the stackViewPlan
getter since everything will be implemented in extensions. It will automatically treat the plan as arrangeSubviews
of the stack:
class MyStack: UIStackView, PlannedStack {
@LayoutPlan
var stackViewPlan: ViewPlan {
UIImageView(image: UIImage(named: "icon_test"))
.sized(CGSize(sides: 56))
UILabel(text: "title text")
UILabel(text: "subtitle text")
}
override init(frame: CGRect) {
super.init(frame: frame)
applyPlan()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
applyPlan()
}
}
Every time you call applyPlan
, it will always try to recreate the view to be the same as what was declared in viewPlan
.
You can use UIPlannedStack
since its a typealias of UIStackView & PlannedStack
You know how, just clone and do a pull request
link |
Stars: 9 |
Last commit: 2 weeks ago |
ScrollableStackView
SpacerView
Array
of PlanComponent
HStackView
and VStackView
functionSwiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics