Skip to content

Instantly share code, notes, and snippets.

@rnkyr
Last active January 11, 2021 16:18
Show Gist options
  • Select an option

  • Save rnkyr/6601c8a89c3cb0fd4cedde00086752f2 to your computer and use it in GitHub Desktop.

Select an option

Save rnkyr/6601c8a89c3cb0fd4cedde00086752f2 to your computer and use it in GitHub Desktop.
Autolayout DSL
import UIKit
protocol LayoutAnchor {
func constraint(equalTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint
func constraint(greaterThanOrEqualTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint
func constraint(lessThanOrEqualTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint
}
protocol LayoutDimension: LayoutAnchor {
func constraint(equalToConstant c: CGFloat) -> NSLayoutConstraint
func constraint(greaterThanOrEqualToConstant c: CGFloat) -> NSLayoutConstraint
func constraint(lessThanOrEqualToConstant c: CGFloat) -> NSLayoutConstraint
func constraint(equalTo anchor: Self, multiplier m: CGFloat) -> NSLayoutConstraint
}
extension NSLayoutAnchor: LayoutAnchor {}
extension NSLayoutDimension: LayoutDimension {}
class LayoutProperty<Anchor: LayoutAnchor> {
fileprivate let anchor: Anchor
fileprivate let kind: Kind
enum Kind { case leading, trailing, top, bottom, centerX, centerY, width, height }
init(anchor: Anchor, kind: Kind) {
self.anchor = anchor
self.kind = kind
}
}
class LayoutAttribute<Dimension: LayoutDimension>: LayoutProperty<Dimension> {
fileprivate let dimension: Dimension
init(dimension: Dimension, kind: Kind) {
self.dimension = dimension
super.init(anchor: dimension, kind: kind)
}
}
final class LayoutProxy {
lazy var leading = LayoutProperty<NSLayoutXAxisAnchor>(anchor: view.leadingAnchor, kind: .leading)
lazy var trailing = LayoutProperty<NSLayoutXAxisAnchor>(anchor: view.trailingAnchor, kind: .trailing)
lazy var top = LayoutProperty<NSLayoutYAxisAnchor>(anchor: view.topAnchor, kind: .top)
lazy var bottom = LayoutProperty<NSLayoutYAxisAnchor>(anchor: view.bottomAnchor, kind: .bottom)
lazy var centerX = LayoutProperty<NSLayoutXAxisAnchor>(anchor: view.centerXAnchor, kind: .centerX)
lazy var centerY = LayoutProperty<NSLayoutYAxisAnchor>(anchor: view.centerYAnchor, kind: .centerY)
lazy var width = LayoutAttribute<NSLayoutDimension>(dimension: view.widthAnchor, kind: .width)
lazy var height = LayoutAttribute<NSLayoutDimension>(dimension: view.heightAnchor, kind: .height)
private let view: UIView
fileprivate init(view: UIView) {
self.view = view
}
}
extension LayoutAttribute {
@discardableResult
func equal(to constant: CGFloat, priority: UILayoutPriority? = nil, isActive: Bool = true) -> NSLayoutConstraint {
let constraint = dimension.constraint(equalToConstant: constant)
(constraint.firstItem as? UIView)?.layout.update(constraint: constraint, kind: kind)
if let priority = priority {
constraint.priority = priority
}
constraint.isActive = isActive
return constraint
}
@discardableResult
func equal(to otherDimension: Dimension, multiplier: CGFloat, priority: UILayoutPriority? = nil) -> NSLayoutConstraint {
let constraint = dimension.constraint(equalTo: otherDimension, multiplier: multiplier)
(constraint.firstItem as? UIView)?.layout.update(constraint: constraint, kind: kind)
if let priority = priority {
constraint.priority = priority
}
constraint.isActive = true
return constraint
}
@discardableResult
func greaterThanOrEqual(to constant: CGFloat, priority: UILayoutPriority? = nil) -> NSLayoutConstraint {
let constraint = dimension.constraint(greaterThanOrEqualToConstant: constant)
(constraint.firstItem as? UIView)?.layout.update(constraint: constraint, kind: kind)
if let priority = priority {
constraint.priority = priority
}
constraint.isActive = true
return constraint
}
@discardableResult
func lessThanOrEqual(to constant: CGFloat, priority: UILayoutPriority? = nil) -> NSLayoutConstraint {
let constraint = dimension.constraint(lessThanOrEqualToConstant: constant)
(constraint.firstItem as? UIView)?.layout.update(constraint: constraint, kind: kind)
if let priority = priority {
constraint.priority = priority
}
constraint.isActive = true
return constraint
}
}
extension LayoutProperty {
@discardableResult
func equal(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0, multiplier: CGFloat = 1, priority: UILayoutPriority? = nil, isActive: Bool = true) -> NSLayoutConstraint {
let constraint = anchor.constraint(equalTo: otherAnchor, constant: constant).constraintWithMultiplier(multiplier)
(constraint.firstItem as? UIView)?.layout.update(constraint: constraint, kind: kind)
if let priority = priority {
constraint.priority = priority
}
constraint.isActive = isActive
return constraint
}
@discardableResult
func greaterThanOrEqual(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0, priority: UILayoutPriority? = nil) -> NSLayoutConstraint {
let constraint = anchor.constraint(greaterThanOrEqualTo: otherAnchor, constant: constant)
(constraint.firstItem as? UIView)?.layout.update(constraint: constraint, kind: kind)
if let priority = priority {
constraint.priority = priority
}
constraint.isActive = true
return constraint
}
@discardableResult
func lessThanOrEqual(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0, priority: UILayoutPriority? = nil) -> NSLayoutConstraint {
let constraint = anchor.constraint(lessThanOrEqualTo: otherAnchor, constant: constant)
(constraint.firstItem as? UIView)?.layout.update(constraint: constraint, kind: kind)
if let priority = priority {
constraint.priority = priority
}
constraint.isActive = true
return constraint
}
}
extension UIView {
func layout(using closure: (LayoutProxy) -> Void) {
translatesAutoresizingMaskIntoConstraints = false
closure(LayoutProxy(view: self))
}
func layout(in superview: UIView, with insets: UIEdgeInsets = .zero) {
superview.addSubview(self)
layout { proxy in
proxy.bottom.equal(to: superview.bottomAnchor, offsetBy: -insets.bottom)
proxy.top.equal(to: superview.topAnchor, offsetBy: insets.top)
proxy.leading.equal(to: superview.leadingAnchor, offsetBy: insets.left)
proxy.trailing.equal(to: superview.trailingAnchor, offsetBy: -insets.right)
}
}
}
//swiftlint:disable large_tuple
// MARK: - Shorthand operators
func +<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
return (lhs, rhs)
}
func -<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
return (lhs, -rhs)
}
@discardableResult
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: (A, CGFloat)) -> NSLayoutConstraint {
return lhs.equal(to: rhs.0, offsetBy: rhs.1)
}
@discardableResult
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) -> NSLayoutConstraint {
return lhs.equal(to: rhs)
}
@discardableResult
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: (A, CGFloat)) -> NSLayoutConstraint {
return lhs.greaterThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}
@discardableResult
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) -> NSLayoutConstraint {
return lhs.greaterThanOrEqual(to: rhs)
}
@discardableResult
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: (A, CGFloat)) -> NSLayoutConstraint {
return lhs.lessThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}
@discardableResult
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) -> NSLayoutConstraint {
return lhs.lessThanOrEqual(to: rhs)
}
@discardableResult
func <=<D: LayoutDimension>(lhs: LayoutAttribute<D>, rhs: CGFloat) -> NSLayoutConstraint {
return lhs.lessThanOrEqual(to: rhs)
}
@discardableResult
func ==<D: LayoutDimension>(lhs: LayoutAttribute<D>, rhs: CGFloat) -> NSLayoutConstraint {
return lhs.equal(to: rhs)
}
@discardableResult
func ==<D: LayoutDimension>(lhs: LayoutAttribute<D>, rhs: LayoutAttribute<D>) -> NSLayoutConstraint {
return lhs.equal(to: rhs.dimension)
}
@discardableResult
func *=<D: LayoutDimension>(lhs: LayoutAttribute<D>, rhs: (D, CGFloat)) -> NSLayoutConstraint {
return lhs.equal(to: rhs.0, multiplier: rhs.1)
}
@discardableResult
func *=<D: LayoutDimension>(lhs: LayoutAttribute<D>, rhs: (LayoutAttribute<D>, CGFloat)) -> NSLayoutConstraint {
return lhs.equal(to: rhs.0.dimension, multiplier: rhs.1)
}
@discardableResult
func *=<D: LayoutDimension>(lhs: LayoutAttribute<D>, rhs: (LayoutAttribute<D>, CGFloat, UILayoutPriority)) -> NSLayoutConstraint {
return lhs.equal(to: rhs.0.dimension, multiplier: rhs.1, priority: rhs.2)
}
@discardableResult
func >=<D: LayoutDimension>(lhs: LayoutAttribute<D>, rhs: CGFloat) -> NSLayoutConstraint {
return lhs.greaterThanOrEqual(to: rhs)
}
//swiftlint:enable large_tuple
extension UIView {
private struct AssociatedKeys {
static var layout = ""
}
var layout: Layout {
get {
var layout: Layout!
let lookup = objc_getAssociatedObject(self, &AssociatedKeys.layout) as? Layout
if let lookup = lookup {
layout = lookup
} else {
let newLayout = Layout()
self.layout = newLayout
layout = newLayout
}
return layout
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.layout, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func addSubviews(_ views: UIView...) {
views.forEach { addSubview($0) }
}
}
final class Layout: NSObject {
weak var top: NSLayoutConstraint?
weak var bottom: NSLayoutConstraint?
weak var leading: NSLayoutConstraint?
weak var trailing: NSLayoutConstraint?
weak var centerX: NSLayoutConstraint?
weak var centerY: NSLayoutConstraint?
weak var width: NSLayoutConstraint?
weak var height: NSLayoutConstraint?
fileprivate func update<A: LayoutAnchor>(constraint: NSLayoutConstraint, kind: LayoutProperty<A>.Kind) {
switch kind {
case .top: top = constraint
case .bottom: bottom = constraint
case .leading: leading = constraint
case .trailing: trailing = constraint
case .centerX: centerX = constraint
case .centerY: centerY = constraint
case .width: width = constraint
case .height: height = constraint
}
}
}
extension UILayoutPriority {
static let lowered = UILayoutPriority(999)
}
fileprivate extension NSLayoutConstraint {
func constraintWithMultiplier(_ multiplier: CGFloat) -> NSLayoutConstraint {
if multiplier == 1 {
return self
}
return NSLayoutConstraint(
item: self.firstItem!,
attribute: self.firstAttribute,
relatedBy: self.relation,
toItem: self.secondItem,
attribute: self.secondAttribute,
multiplier: multiplier,
constant: self.constant
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment