Programmatically creating Autolayout constraints is famously dense to read, although the strongly-typed visual format makes some easier to parse visually.
With that in mind, which is easier to read?
This:
let subView1 = UIView()
let subView2 = UIView
let constraint = NSLayoutConstraint(item: subView1, attribute: .left, relatedBy: NSLayoutRelation.equal, toItem: subView2, attribute: .right, multiplier: 1.0, constant: -10)
constraint.priority = 900.0
view.addConstraint(constraint)
Or this:1
view.📌(subView1 ⬇️=⬆️ subView2 + 10 ‼️ 900.0)
I wrote this as an exercise after reading a description of Erica Sadun’s talk and some posts of hers on the emoji as function name idea. I happened to read this around the time that I was needing to layout a number of views in code using autolayout and was overwhelmed with the verbosity.
So I started goofing. Basically, I use the “boxed arrow” emoji to indicate which edge is being constrained, and double arrows to indicate height/width. Then and “=” operator to indicate that two edges are constrained.
The “+” operator is overloaded to add a constant to the constraint, and the “‼️” emoji is overloaded to indicate priority. Sadly, I couldn’t find an emoji that was valid for use as an operator and that was as clear as the 📌 for communicating “add constraint, so I named a function “📌”.
Lastly, I probably could have just overloaded “+” again to add the constraints, but I ran out of time and it was simpler to overload ++ for adding different constraints.
Likewise, it would be useful to add “<=”, “>=” operators.
Anyway, I’m pretty happy with this fun project. I think that the example below demonstrates the use the syntax. I have also embedded my full playground as a gist.
and here is the layout code
// add one constraint at a time
view.📌(grayView |⬅️ 20.0) //to superview margin
view.📌(grayView -⬆️ 20.0)
view.📌(grayView ⬇️- 20.0)
view.📌(grayView ➡️| 20.0)
// add multiple constraints (combined with ++ operator)
view.📌(redButton |⬅️ 60 ++ redButton -⬆️ 60 ++ redButton ↕️= 40 ++ redButton ↔️= 40)
// can add constants
view.📌(redButton ↔️=↔️ blueButton + 20)
// can add priorities
view.📌(redButton ⬇️=⬆️ blueButton + 100 ‼️ 800.0)
view.📌(redButton ⬇️=⬆️ blueButton + 10 ‼️ 900.0)
view.📌(redButton |=| blueButton) //align vertical centers
view.📌(blueButton ↕️= 100)
view.📌((blueButton--orangeButton + 40) ++ (blueButton-=-orangeButton)) //precedence rules require parentheses around constraints that have constants or priorities
orangeButton.📌(orangeButton ↕️= 45 ++ orangeButton ↔️= 25)
complete gist:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Playground – noun: a place where people can play | |
import UIKit | |
import PlaygroundSupport | |
///create a constraint of view to its superView | |
infix operator ➡️|: AdditionPrecedence | |
infix operator |⬅️: AdditionPrecedence | |
infix operator -⬆️: AdditionPrecedence | |
infix operator ⬇️-: AdditionPrecedence | |
///create a constraint between the designated edge of view to the designated edge of another view | |
infix operator ➡️=⬅️: TernaryPrecedence | |
infix operator –: TernaryPrecedence //same as ➡️=⬅️. it's just clearer | |
infix operator ⬇️=⬆️: TernaryPrecedence | |
infix operator ⬆️=⬆️: TernaryPrecedence | |
infix operator ⬇️=⬇️: TernaryPrecedence | |
infix operator ➡️=➡️: TernaryPrecedence | |
infix operator ⬅️=⬅️: TernaryPrecedence | |
infix operator ↔️=↔️: TernaryPrecedence | |
infix operator ↕️=↕️: TernaryPrecedence | |
///create a constraint between horizontal centers of two views | |
infix operator -=-: TernaryPrecedence | |
///create a constraint between vertical centers of two views | |
infix operator |=|: TernaryPrecedence | |
///create a width constraint on a view | |
infix operator ↔️=: AdditionPrecedence | |
///create a height constraint on a view | |
infix operator ↕️=: AdditionPrecedence | |
///constrain every edge a view to another view | |
infix operator ⏹=⏹: AdditionPrecedence | |
extension UIView { | |
///add a constraint of view to its superView | |
func 📌<T: Any>(_ oneOrMany: T) { | |
if let one = oneOrMany as? NSLayoutConstraint { | |
addConstraint(one) | |
} else if let many = oneOrMany as? [NSLayoutConstraint] { | |
addConstraints(many) | |
} else { | |
print(oneOrMany.self) | |
} | |
} | |
} | |
infix operator +: AdditionPrecedence | |
func +(left: UIView, right: CGFloat) -> ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint) { | |
let returnBlock = {(view, attribute1, attribute2) -> NSLayoutConstraint in | |
return NSLayoutConstraint(item: view, attribute: attribute1, relatedBy: NSLayoutRelation.equal, toItem: left, attribute: attribute2, multiplier: 1.0, constant: -right) | |
} | |
return returnBlock | |
} | |
func +(left: UIView, right: (CGFloat, CGFloat)) -> ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint) { | |
let returnBlock: ((UIView, NSLayoutAttribute, NSLayoutAttribute) -> NSLayoutConstraint) = {(view, attribute1, attribute2) -> NSLayoutConstraint in | |
let constraint = NSLayoutConstraint(item: view, attribute: attribute1, relatedBy: NSLayoutRelation.equal, toItem: left, attribute: attribute2, multiplier: 1.0, constant: -right.0) | |
constraint.priority = UILayoutPriority(right.1) | |
return constraint | |
} | |
return returnBlock | |
} | |
infix operator ++: TernaryPrecedence | |
func ++(left: NSLayoutConstraint, right: NSLayoutConstraint) -> [NSLayoutConstraint] { | |
return [left, right] | |
} | |
func ++(left: NSLayoutConstraint, right: [NSLayoutConstraint]) -> [NSLayoutConstraint] { | |
return [left] + right | |
} | |
func ++(left: [NSLayoutConstraint], right: [NSLayoutConstraint]) -> [NSLayoutConstraint] { | |
return left + right | |
} | |
func ++(left: [NSLayoutConstraint], right: NSLayoutConstraint) -> [NSLayoutConstraint] { | |
return left + [right] | |
} | |
func ++<T: Any>(_ left: T, right: T) -> [NSLayoutConstraint] { | |
if let l = left as? NSLayoutConstraint { | |
if let r = right as? NSLayoutConstraint { | |
return [l, r] | |
} else if let r = right as? [NSLayoutConstraint] { | |
return [l] + r | |
} | |
} else if let l = left as? [NSLayoutConstraint] { | |
if let r = right as? NSLayoutConstraint { | |
return l + [r] | |
} else if let r = right as? [NSLayoutConstraint] { | |
return l + r | |
} | |
} | |
fatalError() | |
} | |
infix operator ‼️: MultiplicationPrecedence | |
func ‼️(left: CGFloat, right: Double) -> (CGFloat, CGFloat) { | |
return (left, CGFloat(right)) | |
} | |
infix operator -: AdditionPrecedence | |
func -(left: UIView, right: CGFloat) -> ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint) { | |
let returnBlock = {(view, attribute1, attribute2) -> NSLayoutConstraint in | |
return NSLayoutConstraint(item: view, attribute: attribute1, relatedBy: NSLayoutRelation.equal, toItem: left, attribute: attribute2, multiplier: 1.0, constant: right) | |
} | |
return returnBlock | |
} | |
func -(left: UIView, right: (CGFloat, CGFloat)) -> ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint) { | |
let returnBlock: ((UIView, NSLayoutAttribute, NSLayoutAttribute) -> NSLayoutConstraint) = {(view, attribute1, attribute2) -> NSLayoutConstraint in | |
let constraint = NSLayoutConstraint(item: view, attribute: attribute1, relatedBy: NSLayoutRelation.equal, toItem: left, attribute: attribute2, multiplier: 1.0, constant: right.0) | |
constraint.priority = UILayoutPriority(right.1) | |
return constraint | |
} | |
return returnBlock | |
} | |
func ➡️| (left: UIView, right: CGFloat) -> NSLayoutConstraint { | |
return NSLayoutConstraint.constraints(withVisualFormat: "H:[left]-\(right)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["left": left]).first! | |
} | |
func |⬅️ (left: UIView, right: CGFloat) -> NSLayoutConstraint { | |
return NSLayoutConstraint.constraints(withVisualFormat: "H:|-\(right)-[left]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["left": left]).first! | |
} | |
func -⬆️ (left: UIView, right: CGFloat) -> NSLayoutConstraint { | |
return NSLayoutConstraint.constraints(withVisualFormat: "V:|-\(right)-[left]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["left": left]).first! | |
} | |
func ⬇️- (left: UIView, right: CGFloat) -> NSLayoutConstraint { | |
return NSLayoutConstraint.constraints(withVisualFormat: "V:[left]-\(right)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["left": left]).first! | |
} | |
func ➡️=⬅️ (left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ➡️=⬅️ right + 0 | |
} | |
func ➡️=⬅️ (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .trailing, .leading) | |
} | |
func –(left: UIView, right: UIView) -> NSLayoutConstraint {//it's just too obvious not to include it | |
return left ➡️=⬅️ right + 0 | |
} | |
func –(left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .trailing, .leading) | |
} | |
func ⬇️=⬆️ (left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ⬇️=⬆️ right + 0 | |
} | |
func ⬇️=⬆️ (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .bottom, .top) | |
} | |
func ⬆️=⬆️ (left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ⬆️=⬆️ right + 0 | |
} | |
func ⬆️=⬆️ (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .top, .top) | |
} | |
func ⬇️=⬇️ (left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ⬇️=⬇️ (right + 0) | |
} | |
func ⬇️=⬇️ (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .bottom, .bottom) | |
} | |
func ➡️=➡️ (left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ➡️=➡️ (right + 0) | |
} | |
func ➡️=➡️ (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .right, .right) | |
} | |
func ⬅️=⬅️ (left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ⬅️=⬅️ (right + 0) | |
} | |
func ⬅️=⬅️ (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .left, .left) | |
} | |
func ↔️=↔️ (left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ↔️=↔️ right + 0 | |
} | |
func ↔️=↔️(left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .width, .width) | |
} | |
func ↕️=↕️(left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left ↕️=↕️ right + 0 | |
} | |
func ↕️=↕️ (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .height, .height) | |
} | |
func -=-(left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left -=- right + 0 | |
} | |
func -=- (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .centerY, .centerY) | |
} | |
func |=|(left: UIView, right: UIView) -> NSLayoutConstraint { | |
return left |=| right + 0 | |
} | |
func |=| (left: UIView, right: ((UIView, NSLayoutAttribute, NSLayoutAttribute) ->NSLayoutConstraint)) -> NSLayoutConstraint { | |
return right(left, .centerX, .centerX) | |
} | |
func ↔️= (view: UIView, width: CGFloat) -> NSLayoutConstraint { | |
return NSLayoutConstraint.constraints(withVisualFormat: "H:[view(==\(width))]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["view": view]).first! | |
} | |
func ↕️= (view: UIView, height: CGFloat) -> NSLayoutConstraint { | |
return NSLayoutConstraint.constraints(withVisualFormat: "V:[view(==\(height))]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["view": view]).first! | |
} | |
func ⏹=⏹ (left: UIView, right: UIView) -> [NSLayoutConstraint] { | |
return [ | |
left ⬆️=⬆️ right, | |
left ⬇️=⬇️ right, | |
left ⬅️=⬅️ right, | |
left ➡️=➡️ right, | |
] | |
} | |
//transforms ↩️↪️ | |
//animations?🔂 | |
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 375.0, height: 300)) | |
PlaygroundPage.current.liveView = containerView | |
containerView.backgroundColor = .brown | |
let redButton = UIButton() | |
redButton.backgroundColor = .red | |
redButton.setTitleColor(.blue, for: .normal) | |
redButton.setTitleColor(.blue, for: .selected) | |
redButton.translatesAutoresizingMaskIntoConstraints = false | |
let blueButton = UIButton() | |
blueButton.translatesAutoresizingMaskIntoConstraints = false | |
blueButton.backgroundColor = .blue | |
let orangeButton = UIButton() | |
orangeButton.translatesAutoresizingMaskIntoConstraints = false | |
orangeButton.backgroundColor = .orange | |
let grayView = UIView() | |
grayView.translatesAutoresizingMaskIntoConstraints = false | |
grayView.backgroundColor = .lightGray | |
containerView.addSubview(grayView) | |
containerView.addSubview(redButton) | |
containerView.addSubview(blueButton) | |
containerView.addSubview(orangeButton) | |
containerView.📌(grayView |⬅️ 20.0) | |
containerView.📌(grayView -⬆️ 20.0) | |
containerView.📌(grayView ⬇️- 20.0) | |
containerView.📌(grayView ➡️| 20.0) | |
containerView.📌(redButton |⬅️ 60 ++ redButton -⬆️ 60 ++ redButton ↕️= 40 ++ redButton ↔️= 40) | |
containerView.📌(redButton ⬇️=⬆️ blueButton + 100 ‼️ 800.0) | |
containerView.📌(redButton ⬇️=⬆️ blueButton + 10 ‼️ 900.0) //overrides previous due to precedence | |
containerView.📌(redButton |=| blueButton) //align vertical centers | |
containerView.📌(redButton ↔️=↔️ blueButton + 20) | |
containerView.📌(blueButton ↕️= 100) | |
containerView.📌((blueButton–orangeButton + 40) ++ (blueButton-=-orangeButton)) //precedence requires parentheses around constraints with other operators | |
orangeButton.📌(orangeButton ↕️= 45 ++ orangeButton ↔️= 25) |
- I certainly agree that the advantage is purely in readability. typing these would only be feasible with a series of textExpander/Xcode snippets ↩︎