Autolayout Constraints in Emoji — Maximum Readability

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:


//: 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)

view raw

Emoji-Layout

hosted with ❤ by GitHub


  1. I certainly agree that the advantage is purely in readability. typing these would only be feasible with a series of textExpander/Xcode snippets ↩︎

Leave a comment