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.
Example
To run the example project, clone the repo, and run pod install from the Example directory first.
Requirements
Swift 5.5 or higher
iOS 12.0 or higher
XCode 13 or higher
Installation
Cocoapods
Draftsman is available through CocoaPods. To install
it, simply add the following line to your Podfile:
pod 'Draftsman', '~> 3.1.1'
Swift Package Manager from XCode
Add it using XCode menu File > Swift Package > Add Package Dependency
Draftsman is available under the MIT license. See the LICENSE file for more info.
Basic Usage
Draftsman is the NSLayoutConstraints and UIView hierarchy builder. Draftsman uses a new resultBuilder from Swift that makes the Declarative approach possible.
Basic
Creating constraints is very easy. All you need to do is call drf to get the LayoutDraft object:
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()
}
}
View Hierarchy
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.
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()
new UIView then will create constraints
new UIView then will insert myView
myView then will create constraints
all the constraints then will be created and activated
So 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:
any descendant of UIView
any descendant of 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.
Using Builder
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:
basic position anchors available from Draftsman are:
top
left
bottom
right
centerX
centerY
leading
trailing
All are available for both UIView and UILayoutGuide
This can be used to create a constraint using one of these three methods:
equal(to:)
moreThan(to:)
lessThan(to:)
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 andinset` 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.
Basic Dimensioning
Dimensioning a View is easy. You just need to declare which anchor should have relation to others or constant:
basic dimension anchors available from Draftsman are:
height
width
All are available for both UIView and UILayoutGuide
This can be used to create a constraint using one of these three methods:
equal(to:)
moreThan(to:)
lessThan(to:)
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.
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:
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:
VerticalOffsets for vertical anchors offsets
VerticalInsets for vertical anchors insets
HorizontalOffsets for horizontal anchors offsets
HorizontalInsets for horizontal anchors insets
AxisOffsets for cross position anchors offsets which are just a typealias of CGPoint
AxisInsets for cross position anchors insets which are just a typealias of CGPoint
EdgeOffsets for 3 and 4 position anchors offsets which is just a typealias of UIEdgeInsets
EdgeInsets for 3 and 4 position anchors insets which is just a typealias of UIEdgeInsets
Implicit Relation
You can pass just UIView or UILayoutGuide instead of Anchor explicitly and it will use the same anchor to make constraints:
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.
Anonymous Anchor
Sometimes you don’t want or even can’t use anchor explicitly. In those cases, you can always use AnonymousLayout:
mySelf which will automatically get the current view
parent which will automatically get the current superview
safeArea which will automatically get the current superview safeAreaLayoutGuide
keyboard which will automatically get the keyboardLayoutGuide (powered by Clavier)
keyboardSafeArea which will automatically get the keyboardLayoutGuide with safeArea (powered by Clavier)
previous which will automatically get the previous view
previousSafeArea which will automatically get the previous safeAreaLayoutGuide
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:
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.
Layout Helper
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
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:
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:
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:
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:
UIPlannedController which is UIViewController & Planned
UIPlannedView which is UIView & Planned
Planned Cell
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:
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:
UITablePlannedCell which is UITableViewCell & PlannedCell
UICollectionPlannedCell which is UICollectionViewCell & PlannedCell
Planned Stack
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:
Draftsman
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.
Example
To run the example project, clone the repo, and run
pod install
from the Example directory first.Requirements
Installation
Cocoapods
Draftsman is available through CocoaPods. To install it, simply add the following line to your Podfile:
Swift Package Manager from XCode
Swift Package Manager from Package.swift
Add as your target dependency in Package.swift
Use it in your target as a
Draftsman
Author
Nayanda Haberty, hainayanda@outlook.com
License
Draftsman is available under the MIT license. See the LICENSE file for more info.
Basic Usage
Draftsman is the
NSLayoutConstraints
andUIView
hierarchy builder. Draftsman uses a new resultBuilder from Swift that makes the Declarative approach possible.Basic
Creating constraints is very easy. All you need to do is call
drf
to get theLayoutDraft
object:there are two methods to end planning constraints which can be called from both any
UIView
orUIViewController
:func apply() -> [NSLayoutConstraint]
func build() -> [NSLayoutConstraint]
the difference between the two is
apply
will activate the constraints butbuild
will only create constraints without activating them. Apply return value is discardable so it’s optional for you to use the createdNSLayoutConstraint
or not.You could always create a
UIViewController
orUIView
and implement thePlanned
protocol, and callapplyPlan()
whenever you want theviewPlan
to be applied:ViewPlan
can always be composed to make the code cleaner:View Hierarchy
You can create a view hierarchy while creating constraints by using the
draftContent
ordrf.insert
method andinsert
method for the subview draft (draftStackedContent
ordrf.insertStacked
andinsertStacked
if its arranged subviews inUIStackView
). Don’t forget to callapply()
orbuild()
, Both will rearrange the view hierarchy but onlyapply()
will activate the constraints created.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 newUIView()
UIView
then will create constraintsUIView
then will insertmyView
myView
then will create constraintsSo if the hierarchy is written in pseudo hierarchy style, it should be similar to this:
the compatible type to be passed in the closure are:
UIView
UIViewController
If you pass
UIViewController
, it will be automatically added theUIViewController
view as a child and put theUIViewController
as a child of its currentUIViewController
. You could insert components as much as you need, it will fit all the Views just like how you write them.Using Builder
You can build your view using Builder library built-in in the Draftsman by calling the
builder
property and get back to Draftsman by callingdrf
again:Basic Positioning
Positioning a View is easy. You just need to declare which anchor should have relation to others:
basic position anchors available from Draftsman are:
All are available for both
UIView
andUILayoutGuide
This can be used to create a constraint using one of these three methods:Those methods can accept basic
NSLayoutAnchor
fromUIKit
or useAnchor
fromDraftsman
as long it’s in the same Axis. To add a constant, use one of theoffset(by:)
orinset(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.
Basic Dimensioning
Dimensioning a View is easy. You just need to declare which anchor should have relation to others or constant:
basic dimension anchors available from Draftsman are:
All are available for both
UIView
andUILayoutGuide
This can be used to create a constraint using one of these three methods:Those methods can accept basic
NSLayoutDimension
fromUIKit
or use dimensionAnchor
fromDraftsman
. To add a constant, use one of theadded(by:)
,substracted(by:)
, ormultiplied(by: )
methods. You can then add priority or/and an identifier for the constraints created.Dimensioning can be achieved using constant too:
Very similar except it accepts
CGFloat
Combining Two or More Anchors
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:
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:
Sizing with size or width.height can be achieved by using
CGSize
too if needed: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
Implicit Relation
You can pass just
UIView
orUILayoutGuide
instead ofAnchor
explicitly and it will use the same anchor to make constraints:In the example above, it will create equal constraints between
myView
vertical anchors andotherView
vertical anchors, then it will create another withmyView
bottom andview.safeAreaLayoutGuide
bottom.Anonymous Anchor
Sometimes you don’t want or even can’t use anchor explicitly. In those cases, you can always use
AnonymousLayout
: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:
available explicit anchors are:
Layout Constraints Shortcuts
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)
Custom View
SpacerView
You can use SpacerView as a Spacer for UIStackView content:
or leave the init empty if you want the spacer size to be dynamic:
ScrollableStackView
There are custom
UIView
namedScrollableStackView
which is aUIStackView
insideUIScrollView
. You can use it if you need aUIStackView
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 regularUIScrollView
minus the capability to change its distribution, since it needed to make sure the view behaves as it should.Layout Helper
Some helpers can be used if you want your
viewPlan
shorter and less explicit. This helper will acceptLayoutPlan
closure so you don’t need to access it viadrf
but directly on its initHStacked and VStacked
HStacked
andVStacked
are a shortcut to create vertical and horizontal UIStackView without creating it explicitly. It has 3 public init that can be used:Example:
This will be equivalent with:
HScrollableStacked and VScrollableStacked
HScrollableStacked
andVScrollableStacked
are a shortcut to create vertical and horizontalScrollableStackView
without creating it explicitly. It has 3 public init that can be used:Example:
This will be equivalent with:
Margined
Margined
is a simple way to add a margin to anyUIView
. Example:This will be equivalent with:
Draftsman Planned
Draftsman
Planned
protocol is the protocol that makes anyUIView
orUIViewController
can have its predefined view plan and applied it using theapplyPlan
method. The protocol is declared like this:The only thing you need to implement is the
viewPlan
getter since everything will be implemented in extensions:Every time you call
applyPlan
, it will always try to recreate the view to be the same as what was declared inviewPlan
.There are some typealias with
Planned
that you can use:UIViewController & Planned
UIView & Planned
Planned Cell
PlannedCell
isPlanned
built specifically for a cell which declared like this:The only thing you need to implement is the
contentViewPlan
getter since everything will be implemented in extensions. It will skipcontentView
and straight into its content:Every time you call
applyPlan
, it will always try to recreate the view to be the same as what was declared inviewPlan
.There are some typealias with
Planned
that you can use:UITableViewCell & PlannedCell
UICollectionViewCell & PlannedCell
Planned Stack
PlannedStack
isPlanned
built specifically for a cell which declared like this:The only thing you need to implement is the
stackViewPlan
getter since everything will be implemented in extensions. It will automatically treat the plan asarrangeSubviews
of the stack:Every time you call
applyPlan
, it will always try to recreate the view to be the same as what was declared inviewPlan
.You can use
UIPlannedStack
since its a typealias ofUIStackView & PlannedStack
Contribute
You know how, just clone and do a pull request