Last active
December 2, 2021 12:09
-
-
Save smosko/0032847d432bb5c560f96cedeb80b3bf to your computer and use it in GitHub Desktop.
AutoLayout micro DSL
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
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