Based on Chris Eidhof's idea and Marcin Siemaszko's expanded implementation, a programmatic autolayout µframework that supports Keypath–based declarative layout for both views and layout guides
A simple layout without Constrainable:
let view = UIView()
let container = UILayoutGuide()
let firstLabel = UILabel()
let secondLabel = UILabel()
let spacer = UILayoutGuide()
view.addSubview(firstLabel)
view.addSubview(secondLabel)
view.addLayoutGuide(container)
view.addLayoutGuide(spacer)
firstLabel.translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// Container has the same edges as the view's layoutMarginsGuide
NSLayoutConstraint.activate([
container.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
container.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
container.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
container.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
])
// firstLabel and secondLabel are vertically centered in the container, have the same width and are separated by a 20 points spacer
NSLayoutConstraint.activate([
firstLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
firstLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
firstLabel.trailingAnchor.constraint(equalTo: spacer.leadingAnchor),
spacer.widthAnchor.constraint(equalToConstant: 20),
secondLabel.centerYAnchor.constraint(equalTo: firstLabel.centerYAnchor),
secondLabel.leadingAnchor.constraint(equalTo: spacer.trailingAnchor),
secondLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
secondLabel.widthAnchor.constraint(equalTo: firstLabel.widthAnchor),
])
With Constrainable:
let view = UIView()
let container = UILayoutGuide()
let firstLabel = UILabel()
let secondLabel = UILabel()
let spacer = UILayoutGuide()
view.addSubview(firstLabel)
view.addSubview(secondLabel)
view.addLayoutGuide(container)
view.addLayoutGuide(spacer)
// Container has the same edges as the view's layoutMarginsGuide
container.activate(
constraint(edgesTo: view.layoutMarginsGuide)
)
// firstLabel and secondLabel are vertically centered in the container, have the same width and are separated by a 20 points spacer
firstLabel.activate([
constraint(same: \.centerYAnchor, as: container),
constraint(same: \.leadingAnchor, as: container),
constraint(\.trailingAnchor, to: \.leadingAnchor, of: spacer)
])
spacer.activate([
constraint(\.widthAnchor, to: 20)
])
secondLabel.activate([
constraint(same: \.centerYAnchor, as: firstLabel),
constraint(same: \.trailingAnchor, as: container),
constraint(\.leadingAnchor, to: \.trailingAnchor, of: spacer),
constraint(same: \.widthAnchor, as: firstLabel),
])
You can specify the kind of relation between constrainable objects (equal, lessThanOrEqual, greaterThanOrEqual), the constant, the multiplier (even for NSLayoutAnchor), and the layout priority
constraint(\.topAnchor, to: \.bottomAnchor, of: someView, relation: .lessThanOrEqual, offset: 10, multiplier: 0.5, priority: .defaultLow)
Since version 1.0 you can decide to use shorthand for KeyPaths:
constraint(.top, to: .bottom, of: someView)
instead of:
constraint(\.topAnchor, to: \.bottomAnchor, of: someView)
with autocomplete! 🎉
• You can constrain a dimension to a constant:
constraint(.width, to: 10)
constraint(.height, to: 10)
• If you are constraining two objects to the same anchor, you can use the "same" shorthand:
// This:
constraint(.top, to: .top, of: someView)
constraint(.width, to: .width, of: someView)
// Is the same as this:
constraint(same: .top, as: someView)
constraint(same: .width, as: someView)
• You can constrain both dimension at the same time:
// This:
constraint(same: .height, as: someView, multiplier: 2)
constraint(same: .width, as: someView, multiplier: 2)
// Is the same as this:
constraint(sizeAs: someView, multiplier: 2)
• You can constrain all the edges at once (with insets, even):
// This:
constraint(same: .top, as: someView, offset: 10)
constraint(same: .bottom, as: someView, offset: -10)
constraint(same: .leading, as: someView, offset: 10)
constraint(same: .trailing, as: someView, offset: -10)
// Is the same as this:
let padding = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
constraint(edgesTo: someView, with: padding)
Note: The last two functions return an array of constraints instead of a single one!
// WRONG:
someOtherView.activate([
constraint(edgesTo: someView)
])
// RIGHT:
someOtherView.activate(
constraint(edgesTo: someView)
)
// WRONG:
someOtherView.activate([
constraint(sizeAs: someView),
constraint(same: .centerX, as: someView),
constraint(same: .centerY, as: someView)
])
// RIGHT:
someOtherView.activate(
constraint(sizeAs: someView) + [
constraint(same: .centerX, as: someView),
constraint(same: .centerY, as: someView)
])
• For animations, you can store the constraint in a lazy variable:
lazy var animatableCenterY = constraint(same: .centerY, as: someView)(someOtherView)
someOtherView.activate([
... // Other constraints
])
animatableCenterY.isActive = true
animatableCenterY.constant = 100
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}