ConstraintsHolder is a tiny Auto Layout framework based on modern Anchors API that simplifies working with constraints by abstracting away their storage. It is especially useful in case you change constraints often such as in applications with dynamic UI.
To install via Swift Package Manager (SPM) simply do the following:
- From Xcode, select from the menu File > Add Package Dependency
- Paste the URL https://github.com/leofriskey/ConstraintsHolder
Consider a following common example:
private let vw = UIView()
private var vwTopConstraint: NSLayoutConstraint?
...
override func viewDidLoad() {
super.viewDidLoad()
vw.translatesAutoResizingMaskIntoConstraints = false
view.addSubview(vw)
let topConstraint = vw.topAnchor.constraint(equalTo: view.topAnchor, constant: 20)
vwTopConstraint = topConstraint
NSLayoutConstraint.ativate([
topConstraint,
// other constraints
])
}
...
func changeConstraint() {
vwTopConstraint?.constant = 50
view.layoutIfNeeded()
}
As shown above if we wish to change the constraint affecting our view, we need to store reference to it in our ViewController.
And since in most modern apps we often have more than one moving parts this whole thing would very fast become cumbersome and bloat UIViewController
with all references to different constraints affecting different views:
private var vw1TopConstraint: NSLayoutConstraint?
private var vw2LeadingConstraint: NSLayoutConstraint?
...
private var vw5LeadingConstraint: NSLayoutConstraint?
Another example would be de-activating, changing and then re-activating constraint which too requires that we store a reference to that constraint somewhere in our UIView
/UIViewController
and then we would have to unwrap it before activation.
This boilerplate and error-prone pattern can be avoided using ConstraintsHolder framework
private let vw = UIView()
...
override func viewDidLoad() {
super.viewDidLoad()
vw.translatesAutoResizingMaskIntoConstraints = false
view.addSubview(vw)
vw.updateConstraints { holder in
holder.top = vw.topAnchor.constraint(equalTo: view.topAnchor, constant: 20)
holder.activate([
\.top
])
}
}
...
func changeConstraint() {
vw.updateConstraints { holder in
holder.top?.constant = 50
view.layoutIfNeeded()
}
}
This will do the very same thing but we no longer need to store a reference to topAnchor
constraint inside ViewController because we assign it to view's holder
- constraints container.
Besides convinience and reducing boilerplate there are other benefits to this approach such as:
- Error-prone constraints assignment:
vw.updateConstraints { holder in
holder.top = vw.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20)
// fatalError: Can't assign value of ConstraintType.leading to variable of ConstraintType.top
holder.activate([
\.top
])
}
- Error-prone constraint activation/de-activation:
vw.updateConstraints { holder in
holder.top = vw.topAnchor.constraint(equalTo: view.topAnchor, constant: 20)
holder.activate([
\.bottom
])
// fatalError: keyPath passed to activate() contained nil value constraint
}
vw.updateConstraints { holder in
holder.deactivate([
\.bottom
])
// fatalError: keyPath passed to deactivate() contained nil value constraint
}
- Error-prone constraint removal
vw.updateConstraints { holder in
holder.top = nil
// fatalError: top constraint must be deactivated first
}
- Since framework uses keyPaths - you can't mistakenly activate/deactivate another constraint thad doesn't affect your view like in this example below:
private var vw1TopConstraint: NSLayoutConstraint?
private var vw2TopConstraint: NSLayoutConstraint?
...
override func viewDidLoad() {
super.viewDidLoad()
...
guard let vw2TopConstraint else { return }
NSLayoutConstraint.activate([
vw2TopConstraint // should've been vw1
])
}
- Consice and beautiful code. Update view-bound constraints from anywhere!
// just an example piece of code from one of my apps
...
assetsTotalValueLabel.updateConstraints { holder in
// deactivate old constraints
holder.deactivate([
\.centerY,
\.leading
])
// replace old constraints with new
holder.centerY = assetsTotalValueLabel.centerYAnchor.constraint(equalTo: navBar.centerYAnchor)
holder.leading = assetsTotalValueLabel.leadingAnchor.constraint(equalTo: smallTitleView.trailingAnchor, constant: 10)
// activate new constraints
holder.activate([
\.centerY,
\.leading
])
}
- Return all view setted constraints or return only
active
ones:
vw.updateConstraints { holder in
let all = holder.all // [NSLayoutConstraint]
let active = holder.active() // [Constraints.ConstraintType: NSLayoutConstraint]
}
- Constraints are automatically cleared up when view is removed from view hierarchy - so you don't have to do it yourself!
ConstraintsHolder uses accessibilityIdentifier
as a way to identify UIView
s, so if you use it inside your app you better off not using this framework