Skip to content

Instantly share code, notes, and snippets.

@smosko
Last active December 2, 2021 12:09
Show Gist options
  • Save smosko/0032847d432bb5c560f96cedeb80b3bf to your computer and use it in GitHub Desktop.
Save smosko/0032847d432bb5c560f96cedeb80b3bf to your computer and use it in GitHub Desktop.
AutoLayout micro DSL
import UIKit
public protocol LayoutAnchor {}
extension NSLayoutAnchor: LayoutAnchor {}
public struct AnchorPair<T: AnyObject, U: AnyObject>: LayoutAnchor {
public var first: NSLayoutAnchor<T>
public var second: NSLayoutAnchor<U>
}
public struct EdgeAnchors: LayoutAnchor {
public var top: NSLayoutYAxisAnchor
public var leading: NSLayoutXAxisAnchor
public var trailing: NSLayoutXAxisAnchor
public var bottom: NSLayoutYAxisAnchor
}
public protocol LayoutConstant {}
extension CGFloat: LayoutConstant {}
extension NSDirectionalEdgeInsets: LayoutConstant {}
// extension UIEdgeInsets: LayoutConstant {}
// extension UIOffset: LayoutConstant {}
// extension CGSize: LayoutConstant {} // x,y inset?
// extension CGPoint: LayoutConstant {} // x,y offset?
public struct LayoutExpression<A: LayoutAnchor, C: LayoutConstant> {
public let anchor: A?
public var constant: C
public var multiplier: CGFloat = 1
public var priority: UILayoutPriority = .required
}
public extension UIView {
var horizontalAnchors: AnchorPair<NSLayoutXAxisAnchor, NSLayoutXAxisAnchor> {
AnchorPair(first: leadingAnchor, second: trailingAnchor)
}
var verticalAnchors: AnchorPair<NSLayoutYAxisAnchor, NSLayoutYAxisAnchor> {
AnchorPair(first: topAnchor, second: bottomAnchor)
}
var centerAnchors: AnchorPair<NSLayoutXAxisAnchor, NSLayoutYAxisAnchor> {
AnchorPair(first: centerXAnchor, second: centerYAnchor)
}
var sizeAnchors: AnchorPair<NSLayoutDimension, NSLayoutDimension> {
AnchorPair(first: widthAnchor, second: heightAnchor)
}
var edgeAnchors: EdgeAnchors {
EdgeAnchors(top: topAnchor, leading: leadingAnchor, trailing: trailingAnchor, bottom: bottomAnchor)
}
}
public extension UILayoutGuide {
var horizontalAnchors: AnchorPair<NSLayoutXAxisAnchor, NSLayoutXAxisAnchor> {
AnchorPair(first: leadingAnchor, second: trailingAnchor)
}
var verticalAnchors: AnchorPair<NSLayoutYAxisAnchor, NSLayoutYAxisAnchor> {
AnchorPair(first: topAnchor, second: bottomAnchor)
}
var centerAnchors: AnchorPair<NSLayoutXAxisAnchor, NSLayoutYAxisAnchor> {
AnchorPair(first: centerXAnchor, second: centerYAnchor)
}
var sizeAnchors: AnchorPair<NSLayoutDimension, NSLayoutDimension> {
AnchorPair(first: widthAnchor, second: heightAnchor)
}
var edgeAnchors: EdgeAnchors {
EdgeAnchors(top: topAnchor, leading: leadingAnchor, trailing: trailingAnchor, bottom: bottomAnchor)
}
}
// MARK: - Layout expression operators
public func + <A: LayoutAnchor, C: LayoutConstant>(lhs: A, rhs: C) -> LayoutExpression<A, C> {
LayoutExpression(anchor: lhs, constant: rhs)
}
public func + <A: LayoutAnchor, C: LayoutConstant>(lhs: C, rhs: A) -> LayoutExpression<A, C> {
LayoutExpression(anchor: rhs, constant: lhs)
}
public func + <A: LayoutAnchor>(lhs: LayoutExpression<A, CGFloat>, rhs: CGFloat) -> LayoutExpression<A, CGFloat> {
var expression = lhs
expression.constant += rhs
return expression
}
public func + <A: LayoutAnchor>(lhs: CGFloat, rhs: LayoutExpression<A, CGFloat>) -> LayoutExpression<A, CGFloat> {
var expression = rhs
expression.constant += lhs
return expression
}
public func - <A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> LayoutExpression<A, CGFloat> {
LayoutExpression(anchor: lhs, constant: -rhs)
}
public func - <A: LayoutAnchor>(lhs: CGFloat, rhs: A) -> LayoutExpression<A, CGFloat> {
LayoutExpression(anchor: rhs, constant: lhs, multiplier: -1)
}
public func - <A: LayoutAnchor>(lhs: LayoutExpression<A, CGFloat>, rhs: CGFloat) -> LayoutExpression<A, CGFloat> {
var expression = lhs
expression.constant -= rhs
return expression
}
public func - <A: LayoutAnchor>(lhs: CGFloat, rhs: LayoutExpression<A, CGFloat>) -> LayoutExpression<A, CGFloat> {
var expression = rhs
expression.constant += lhs
expression.multiplier.negate()
return expression
}
public func * <A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> LayoutExpression<A, CGFloat> {
LayoutExpression(anchor: lhs, constant: 0, multiplier: rhs)
}
public func * <A: LayoutAnchor>(lhs: CGFloat, rhs: A) -> LayoutExpression<A, CGFloat> {
LayoutExpression(anchor: rhs, constant: 0, multiplier: lhs)
}
public func * <A: LayoutAnchor>(lhs: LayoutExpression<A, CGFloat>, rhs: CGFloat) -> LayoutExpression<A, CGFloat> {
var expression = lhs
expression.multiplier *= rhs
return expression
}
public func * <A: LayoutAnchor>(lhs: CGFloat, rhs: LayoutExpression<A, CGFloat>) -> LayoutExpression<A, CGFloat> {
var expression = rhs
expression.multiplier *= lhs
return expression
}
public func / <A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> LayoutExpression<A, CGFloat> {
LayoutExpression(anchor: lhs, constant: 0, multiplier: 1 / rhs)
}
public func / <A: LayoutAnchor>(lhs: LayoutExpression<A, CGFloat>, rhs: CGFloat) -> LayoutExpression<A, CGFloat> {
var expression = lhs
expression.multiplier /= rhs
return expression
}
// MARK: - Layout priority operator
precedencegroup PriorityPrecedence {
associativity: none
higherThan: ComparisonPrecedence
lowerThan: AdditionPrecedence
}
infix operator ~: PriorityPrecedence
public func ~ (lhs: CGFloat, rhs: UILayoutPriority) -> LayoutExpression<NSLayoutDimension, CGFloat> {
LayoutExpression(anchor: nil, constant: lhs, priority: rhs)
}
public func ~ <A: LayoutAnchor>(lhs: A, rhs: UILayoutPriority) -> LayoutExpression<A, CGFloat> {
LayoutExpression(anchor: lhs, constant: 0, priority: rhs)
}
public func ~ <A, C>(lhs: LayoutExpression<A, C>, rhs: UILayoutPriority) -> LayoutExpression<A, C> {
var expression = lhs
expression.priority = rhs
return expression
}
// MARK: - Layout constraint operators
@discardableResult
public func == (lhs: NSLayoutXAxisAnchor, rhs: NSLayoutXAxisAnchor) -> NSLayoutConstraint {
lhs.constraint(equalTo: rhs).activate()
}
@discardableResult
public func == (lhs: NSLayoutYAxisAnchor, rhs: NSLayoutYAxisAnchor) -> NSLayoutConstraint {
lhs.constraint(equalTo: rhs).activate()
}
@discardableResult
public func == (lhs: NSLayoutDimension, rhs: NSLayoutDimension) -> NSLayoutConstraint {
lhs.constraint(equalTo: rhs).activate()
}
@discardableResult
public func == (lhs: NSLayoutDimension, rhs: CGFloat) -> NSLayoutConstraint {
lhs.constraint(equalToConstant: rhs).activate()
}
@discardableResult
public func == (lhs: NSLayoutXAxisAnchor, rhs: LayoutExpression<NSLayoutXAxisAnchor, CGFloat>) -> NSLayoutConstraint {
let constraint = lhs.constraint(equalTo: rhs.anchor!, constant: rhs.constant)
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
@discardableResult
public func == (lhs: NSLayoutYAxisAnchor, rhs: LayoutExpression<NSLayoutYAxisAnchor, CGFloat>) -> NSLayoutConstraint {
let constraint = lhs.constraint(equalTo: rhs.anchor!, constant: rhs.constant)
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
@discardableResult
public func == (lhs: NSLayoutDimension, rhs: LayoutExpression<NSLayoutDimension, CGFloat>) -> NSLayoutConstraint {
let constraint: NSLayoutConstraint
if let anchor = rhs.anchor {
constraint = lhs.constraint(equalTo: anchor, constant: rhs.constant)
} else {
constraint = lhs.constraint(equalToConstant: rhs.constant)
}
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
public func == <T, U>(lhs: AnchorPair<T, U>, rhs: AnchorPair<T, U>) {
NSLayoutConstraint.batch {
lhs.first.constraint(equalTo: rhs.first).activate()
lhs.second.constraint(equalTo: rhs.second).activate()
}
}
public func == <T, U>(lhs: AnchorPair<T, U>, rhs: LayoutExpression<AnchorPair<T, U>, CGFloat>) {
NSLayoutConstraint.batch {
lhs.first.constraint(equalTo: rhs.anchor!.first, constant: rhs.constant)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
lhs.second.constraint(equalTo: rhs.anchor!.second, constant: -rhs.constant)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
}
}
public func == (lhs: EdgeAnchors, rhs: EdgeAnchors) {
NSLayoutConstraint.batch {
lhs.top.constraint(equalTo: rhs.top).activate()
lhs.leading.constraint(equalTo: rhs.leading).activate()
lhs.trailing.constraint(equalTo: rhs.trailing).activate()
lhs.bottom.constraint(equalTo: rhs.bottom).activate()
}
}
public func == (lhs: EdgeAnchors, rhs: LayoutExpression<EdgeAnchors, CGFloat>) {
NSLayoutConstraint.batch {
lhs.top.constraint(equalTo: rhs.anchor!.top, constant: rhs.constant)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
lhs.leading.constraint(equalTo: rhs.anchor!.leading, constant: rhs.constant)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
lhs.trailing.constraint(equalTo: rhs.anchor!.trailing, constant: -rhs.constant)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
lhs.bottom.constraint(equalTo: rhs.anchor!.bottom, constant: -rhs.constant)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
}
}
public func == (lhs: EdgeAnchors, rhs: LayoutExpression<EdgeAnchors, NSDirectionalEdgeInsets>) {
NSLayoutConstraint.batch {
lhs.top.constraint(equalTo: rhs.anchor!.top, constant: rhs.constant.top)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
lhs.leading.constraint(equalTo: rhs.anchor!.leading, constant: rhs.constant.leading)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
lhs.trailing.constraint(equalTo: rhs.anchor!.trailing, constant: -rhs.constant.trailing)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
lhs.bottom.constraint(equalTo: rhs.anchor!.bottom, constant: -rhs.constant.bottom)
.with(multiplier: rhs.multiplier, priority: rhs.priority)
.activate()
}
}
@discardableResult
public func >= (lhs: NSLayoutXAxisAnchor, rhs: NSLayoutXAxisAnchor) -> NSLayoutConstraint {
lhs.constraint(greaterThanOrEqualTo: rhs).activate()
}
@discardableResult
public func >= (lhs: NSLayoutYAxisAnchor, rhs: NSLayoutYAxisAnchor) -> NSLayoutConstraint {
lhs.constraint(greaterThanOrEqualTo: rhs).activate()
}
@discardableResult
public func >= (lhs: NSLayoutDimension, rhs: NSLayoutDimension) -> NSLayoutConstraint {
lhs.constraint(greaterThanOrEqualTo: rhs).activate()
}
@discardableResult
public func >= (lhs: NSLayoutDimension, rhs: CGFloat) -> NSLayoutConstraint {
lhs.constraint(greaterThanOrEqualToConstant: rhs).activate()
}
@discardableResult
public func >= (lhs: NSLayoutXAxisAnchor, rhs: LayoutExpression<NSLayoutXAxisAnchor, CGFloat>) -> NSLayoutConstraint {
let constraint = lhs.constraint(greaterThanOrEqualTo: rhs.anchor!, constant: rhs.constant)
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
@discardableResult
public func >= (lhs: NSLayoutYAxisAnchor, rhs: LayoutExpression<NSLayoutYAxisAnchor, CGFloat>) -> NSLayoutConstraint {
let constraint = lhs.constraint(greaterThanOrEqualTo: rhs.anchor!, constant: rhs.constant)
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
@discardableResult
public func >= (lhs: NSLayoutDimension, rhs: LayoutExpression<NSLayoutDimension, CGFloat>) -> NSLayoutConstraint {
let constraint: NSLayoutConstraint
if let anchor = rhs.anchor {
constraint = lhs.constraint(greaterThanOrEqualTo: anchor, constant: rhs.constant)
} else {
constraint = lhs.constraint(greaterThanOrEqualToConstant: rhs.constant)
}
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
@discardableResult
public func <= (lhs: NSLayoutXAxisAnchor, rhs: NSLayoutXAxisAnchor) -> NSLayoutConstraint {
lhs.constraint(lessThanOrEqualTo: rhs).activate()
}
@discardableResult
public func <= (lhs: NSLayoutYAxisAnchor, rhs: NSLayoutYAxisAnchor) -> NSLayoutConstraint {
lhs.constraint(lessThanOrEqualTo: rhs).activate()
}
@discardableResult
public func <= (lhs: NSLayoutDimension, rhs: NSLayoutDimension) -> NSLayoutConstraint {
lhs.constraint(lessThanOrEqualTo: rhs).activate()
}
@discardableResult
public func <= (lhs: NSLayoutDimension, rhs: CGFloat) -> NSLayoutConstraint {
lhs.constraint(lessThanOrEqualToConstant: rhs).activate()
}
@discardableResult
public func <= (lhs: NSLayoutXAxisAnchor, rhs: LayoutExpression<NSLayoutXAxisAnchor, CGFloat>) -> NSLayoutConstraint {
let constraint = lhs.constraint(lessThanOrEqualTo: rhs.anchor!, constant: rhs.constant)
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
@discardableResult
public func <= (lhs: NSLayoutYAxisAnchor, rhs: LayoutExpression<NSLayoutYAxisAnchor, CGFloat>) -> NSLayoutConstraint {
let constraint = lhs.constraint(lessThanOrEqualTo: rhs.anchor!, constant: rhs.constant)
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
@discardableResult
public func <= (lhs: NSLayoutDimension, rhs: LayoutExpression<NSLayoutDimension, CGFloat>) -> NSLayoutConstraint {
let constraint: NSLayoutConstraint
if let anchor = rhs.anchor {
constraint = lhs.constraint(lessThanOrEqualTo: anchor, constant: rhs.constant)
} else {
constraint = lhs.constraint(lessThanOrEqualToConstant: rhs.constant)
}
return constraint.with(multiplier: rhs.multiplier, priority: rhs.priority).activate()
}
// MARK: - Constraint activation and batching
extension NSLayoutConstraint {
public static func activate(_ constraints: () -> Void) {
let batch = Batch()
batches.append(batch)
defer {
batches.removeLast()
}
constraints()
batch.activate()
}
@discardableResult
fileprivate func activate() -> NSLayoutConstraint {
if let firstView = firstItem as? UIView {
firstView.translatesAutoresizingMaskIntoConstraints = false
}
if let currentBatch = NSLayoutConstraint.batches.last {
currentBatch.add(self)
} else {
isActive = true
}
return self
}
fileprivate func with(multiplier: CGFloat, priority: UILayoutPriority) -> NSLayoutConstraint {
let constraint = NSLayoutConstraint(
item: firstItem!,
attribute: firstAttribute,
relatedBy: relation,
toItem: secondItem,
attribute: secondAttribute,
multiplier: multiplier,
constant: constant)
constraint.priority = priority
return constraint
}
fileprivate static func batch(_ constraints: () -> Void) {
if batches.isEmpty {
// activate constraints in a new batch
activate(constraints)
} else {
// add constraints to the current batch
constraints()
}
}
private static var batches: [Batch] = []
private class Batch {
var constraints: [NSLayoutConstraint] = []
func add(_ constraint: NSLayoutConstraint) {
constraints.append(constraint)
}
func activate() {
NSLayoutConstraint.activate(constraints)
}
}
}
extension UIView {
public func addSubview(_ view: UIView, constraints: (UIView) -> Void) {
addSubview(view)
NSLayoutConstraint.activate {
constraints(view)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment