Skip to content

Instantly share code, notes, and snippets.

@EmperiorEric
Last active February 19, 2020 01:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save EmperiorEric/bf4d76c550e338c31c31dea076dee7f1 to your computer and use it in GitHub Desktop.
Save EmperiorEric/bf4d76c550e338c31c31dea076dee7f1 to your computer and use it in GitHub Desktop.
A simple extension I wrote that makes working with visual format for AutoLayout much more enjoyable.
//
// NSLayoutConstraint+StandardMetrics.swift
//
// Created by Ryan Poolos on 4/22/15.
// Copyright (c) 2015 Frozen Fire Studios, Inc. All rights reserved.
//
import UIKit
extension Double {
public static var halfPadding = 4.0
public static var padding = 8.0
public static var thickPadding = 12.0
public static var margin = 16.0
public static var thickMargin = 24.0
public static var doubleMargin = 32.0
public static var pixelHeight = 1.0 / Double(UIScreen.main.scale)
}
extension CGFloat {
public static var halfPadding: CGFloat = 4.0
public static var padding: CGFloat = 8.0
public static var thickPadding: CGFloat = 12.0
public static var margin: CGFloat = 16.0
public static var thickMargin: CGFloat = 24.0
public static var doubleMargin: CGFloat = 32.0
public static var pixelHeight: CGFloat = 1.0 / UIScreen.main.scale
}
public let halfPaddingMetricKey = "halfPadding"
public let paddingMetricKey = "padding"
public let thickPaddingMetricKey = "thickPadding"
public let marginMetricKey = "margin"
public let thickMarginMetricKey = "thickMargin"
public let doubleMarginMetricKey = "doubleMargin"
public let pixelHeightMetricKey = "pixel"
private var _standardMetrics: [String: Double] = [
halfPaddingMetricKey: .halfPadding,
paddingMetricKey: .padding,
thickPaddingMetricKey: .thickPadding,
marginMetricKey: .margin,
thickMarginMetricKey: .thickMargin,
doubleMarginMetricKey: .doubleMargin,
pixelHeightMetricKey: .pixelHeight,
]
public extension NSLayoutConstraint {
//==========================================================================
// MARK: - Standard Metrics
//==========================================================================
static var standardMetrics: [String: Double] {
return _standardMetrics
}
class func addStandardMetric(_ key: String, value: Double) {
_standardMetrics[key] = value
}
//==========================================================================
// MARK: - Visual Format Convenience
//==========================================================================
class func constraintsWithFormat(_ format: String, views: [String: Any], metrics: [String: Double] = standardMetrics, options: NSLayoutConstraint.FormatOptions = []) -> [NSLayoutConstraint] {
return constraints(withVisualFormat: format, options: options, metrics: metrics, views: views)
}
@discardableResult class func activeConstraintsWithFormat(_ format: String, views: [String: Any], metrics: [String: Double] = standardMetrics, options: NSLayoutConstraint.FormatOptions = []) -> [NSLayoutConstraint] {
let constraints = constraintsWithFormat(format, views: views, metrics: metrics, options: options)
activate(constraints)
return constraints
}
//==========================================================================
// MARK: - Fill Superview
//==========================================================================
/// Returns inactive constraints for a given view to fill its superview
/// minus the given fixed insets.
class func constraintsToFillSuperview(view: UIView, insets: UIEdgeInsets) -> [NSLayoutConstraint] {
let views = ["view": view]
var constraints: [NSLayoutConstraint] = []
constraints.append(contentsOf: constraintsWithFormat("H:|-(\(insets.left))-[view]-(\(insets.right))-|", views: views))
constraints.append(contentsOf: constraintsWithFormat("V:|-(\(insets.top))-[view]-(\(insets.bottom))-|", views: views))
return constraints
}
/// Returns inactive constraints for a given view to fill its superview
/// minus fixed horizontal and vertical margins.
class func constraintsToFillSuperview(view: UIView, horizontal: CGFloat = 0, vertical: CGFloat = 0) -> [NSLayoutConstraint] {
return constraintsToFillSuperview(view: view, insets: UIEdgeInsets(horizontal: horizontal, vertical: vertical))
}
/// Returns inactive constraints for a given view to fill its superview
/// minus fixed equal margins.
class func constraintsToFillSuperview(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
return constraintsToFillSuperview(view: view, insets: UIEdgeInsets(all: margin))
}
/// Returns already active constraints for a given view to fill its
/// superview minus the given fixed insets.
@discardableResult class func activeConstraintsToFillSuperview(view: UIView, insets: UIEdgeInsets) -> [NSLayoutConstraint] {
let constraints = constraintsToFillSuperview(view: view, insets: insets)
activate(constraints)
return constraints
}
/// Returns already active constraints for a given view to fill its
/// superview minus fixed horizontal and vertical margins.
@discardableResult class func activeConstraintsToFillSuperview(view: UIView, horizontal: CGFloat = 0, vertical: CGFloat = 0) -> [NSLayoutConstraint] {
let constraints = constraintsToFillSuperview(view: view, horizontal: horizontal, vertical: vertical)
activate(constraints)
return constraints
}
/// Returns already active constraints for a given view to fill its superview
/// minus fixed equal margins.
@discardableResult class func activeConstraintsToFillSuperview(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
let constraints = constraintsToFillSuperview(view: view, margin: margin)
activate(constraints)
return constraints
}
/// Sets the given view to fill its superview horizontally minus a fixed margin
class func constraintsToHorizontallyFillSuperview(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
let views = ["view": view]
return constraintsWithFormat("H:|-(\(margin))-[view]-(\(margin))-|", views: views)
}
/// Sets the given view to fill its superview horizontally minus a fixed margin
@discardableResult class func activeConstraintsToHorizontallyFillSuperview(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
let views = ["view": view]
return activeConstraintsWithFormat("H:|-(\(margin))-[view]-(\(margin))-|", views: views)
}
/// Sets the given view to fill its superview vertically minus a fixed margin
class func constraintsToVerticallyFillSuperview(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
let views = ["view": view]
return constraintsWithFormat("V:|-(\(margin))-[view]-(\(margin))-|", views: views)
}
/// Sets the given view to fill its superview vertically minus a fixed margin
@discardableResult class func activeConstraintsToVerticallyFillSuperview(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
let views = ["view": view]
return activeConstraintsWithFormat("V:|-(\(margin))-[view]-(\(margin))-|", views: views)
}
/// Sets the given view to fill its superview vertically minus a fixed margin
class func constraintsToVerticallyFillSuperviewSafeArea(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
guard let superview = view.superview else {
return []
}
return [
view.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor, constant: margin),
view.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor, constant: -margin),
]
}
/// Sets the given view to fill its superview vertically minus a fixed margin
@discardableResult class func activeConstraintsToVerticallyFillSuperviewSafeArea(view: UIView, margin: CGFloat = 0) -> [NSLayoutConstraint] {
let constraints = constraintsToVerticallyFillSuperviewSafeArea(view: view, margin: margin)
activate(constraints)
return constraints
}
//==========================================================================
// MARK: - Contain In Superview
//==========================================================================
/// Sets the given view to be contained within its superview plus a greater than or equal to the minimum margin given.
class func constraintsToContainInSuperview(view: UIView, minimumMargin: CGFloat = 0) -> [NSLayoutConstraint] {
let views = ["view": view]
var constraints: [NSLayoutConstraint] = []
constraints.append(contentsOf: constraintsWithFormat("H:|-(>=\(minimumMargin))-[view]-(>=\(minimumMargin))-|", views: views))
constraints.append(contentsOf: constraintsWithFormat("V:|-(>=\(minimumMargin))-[view]-(>=\(minimumMargin))-|", views: views))
return constraints
}
/// Sets the given view to be contained within its superview plus a greater than or equal to the minimum margin given.
@discardableResult class func activeConstraintsToContainInSuperview(view: UIView, minimumMargin: CGFloat = 0) -> [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
constraints.append(contentsOf: constraintsToContainVerticallyInSuperview(view: view, minimumMargin: minimumMargin))
constraints.append(contentsOf: constraintsToContainHorizontallyInSuperview(view: view, minimumMargin: minimumMargin))
activate(constraints)
return constraints
}
/// Sets the given view to be contained horizontally within its superview plus a greater than or equal to the minimum margin given.
class func constraintsToContainHorizontallyInSuperview(view: UIView, minimumMargin: CGFloat = 0) -> [NSLayoutConstraint] {
let views = ["view": view]
return constraintsWithFormat("H:|-(>=\(minimumMargin))-[view]-(>=\(minimumMargin))-|", views: views)
}
/// Sets the given view to be contained horizontally within its superview plus a greater than or equal to the minimum margin given.
@discardableResult class func activeConstraintsToContainHorizontallyInSuperview(view: UIView, minimumMargin: CGFloat = 0) -> [NSLayoutConstraint] {
let constraints = constraintsToContainHorizontallyInSuperview(view: view, minimumMargin: minimumMargin)
activate(constraints)
return constraints
}
/// Sets the given view to be contained vertically within its superview plus a greater than or equal to the minimum margin given.
class func constraintsToContainVerticallyInSuperview(view: UIView, minimumMargin: CGFloat = 0) -> [NSLayoutConstraint] {
let views = ["view": view]
return constraintsWithFormat("V:|-(>=\(minimumMargin))-[view]-(>=\(minimumMargin))-|", views: views)
}
/// Sets the given view to be contained vertically within its superview plus a greater than or equal to the minimum margin given.
@discardableResult class func activeConstraintsToContainVerticallyInSuperview(view: UIView, minimumMargin: CGFloat = 0) -> [NSLayoutConstraint] {
let constraints = constraintsToContainVerticallyInSuperview(view: view, minimumMargin: minimumMargin)
activate(constraints)
return constraints
}
//==========================================================================
// MARK: - Center In Superview
//==========================================================================
/// Centers a given view within its superview with optional offset
class func constraintsToCenterInSuperview(view: UIView, offset: CGSize = .zero) -> [NSLayoutConstraint] {
guard let superview = view.superview else {
fatalError("View must have superview to center in superview: \(view)")
}
let constraints: [NSLayoutConstraint] = [
view.centerXAnchor.constraint(equalTo: superview.centerXAnchor, constant: offset.width),
view.centerYAnchor.constraint(equalTo: superview.centerYAnchor, constant: offset.height),
]
return constraints
}
/// Centers a given view within its superview with optional offset
@discardableResult class func activeConstraintsToCenterInSuperview(view: UIView, offset: CGSize = .zero) -> [NSLayoutConstraint] {
let constraints = constraintsToCenterInSuperview(view: view, offset: offset)
activate(constraints)
return constraints
}
/// Centers a given view based on the center of another given view with optional offset.
class func constraintsToCenter(view: Anchorable, to otherView: Anchorable, offset: CGSize = .zero) -> [NSLayoutConstraint] {
let constraints: [NSLayoutConstraint] = [
view.centerXAnchor.constraint(equalTo: otherView.centerXAnchor, constant: offset.width),
view.centerYAnchor.constraint(equalTo: otherView.centerYAnchor, constant: offset.height),
]
return constraints
}
/// Centers a given view based on the center of another given view with optional offset.
@discardableResult class func activeConstraintsToCenter(view: Anchorable, to otherView: Anchorable, offset: CGSize = .zero) -> [NSLayoutConstraint] {
let constraints = constraintsToCenter(view: view, to: otherView, offset: offset)
activate(constraints)
return constraints
}
//==========================================================================
// MARK: - Pin Edges to Other View
//==========================================================================
/// Matches a views left, right, top, or bottom edges to another view, with optional insets.
class func constraints(for view: Anchorable, toPinTo otherView: Anchorable, edges: UIRectEdge = .all, insets: UIEdgeInsets = .zero) -> [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
if edges.contains(.left) {
constraints.append(view.leftAnchor.constraint(equalTo: otherView.leftAnchor, constant: insets.left))
}
if edges.contains(.right) {
constraints.append(view.rightAnchor.constraint(equalTo: otherView.rightAnchor, constant: -insets.right))
}
if edges.contains(.top) {
constraints.append(view.topAnchor.constraint(equalTo: otherView.topAnchor, constant: insets.top))
}
if edges.contains(.bottom) {
constraints.append(view.bottomAnchor.constraint(equalTo: otherView.bottomAnchor, constant: -insets.bottom))
}
return constraints
}
/// Matches a views left, right, top, or bottom edges to another view, with optional insets.
@discardableResult class func activeConstraints(for view: Anchorable, toPinTo otherView: Anchorable, edges: UIRectEdge = .all, insets: UIEdgeInsets = .zero) -> [NSLayoutConstraint] {
let constraint = constraints(for: view, toPinTo: otherView, edges: edges, insets: insets)
activate(constraint)
return constraint
}
/// Matches a views width and height to another view, with optional insets.
class func constraints(for view: Anchorable, toMatchSizeOf otherView: Anchorable, insets: UIEdgeInsets = .zero) -> [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
constraints.append(view.widthAnchor.constraint(equalTo: otherView.widthAnchor, constant: -insets.horizontalInsets))
constraints.append(view.heightAnchor.constraint(equalTo: otherView.heightAnchor, constant: -insets.verticalInsets))
return constraints
}
/// Matches a views width and height to another view, with optional insets.
@discardableResult class func activeConstraints(for view: Anchorable, toMatchSizeOf otherView: Anchorable, insets: UIEdgeInsets = .zero) -> [NSLayoutConstraint] {
let constraint = constraints(for: view, toMatchSizeOf: otherView, insets: insets)
activate(constraint)
return constraint
}
func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint {
self.priority = priority
return self
}
}
public protocol Anchorable {
var leadingAnchor: NSLayoutXAxisAnchor { get }
var trailingAnchor: NSLayoutXAxisAnchor { get }
var leftAnchor: NSLayoutXAxisAnchor { get }
var rightAnchor: NSLayoutXAxisAnchor { get }
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
var widthAnchor: NSLayoutDimension { get }
var heightAnchor: NSLayoutDimension { get }
var centerXAnchor: NSLayoutXAxisAnchor { get }
var centerYAnchor: NSLayoutYAxisAnchor { get }
}
extension UILayoutGuide: Anchorable {}
extension UIView: Anchorable {}
@EmperiorEric
Copy link
Author

Example Usage

        view.addSubview(blurView)
        view.addSubview(errorStack)
        view.addSubview(collectionView)
        view.addSubview(loadingIndicator)

        NSLayoutConstraint.activeConstraintsToFillSuperview(view: blurView)
        NSLayoutConstraint.activeConstraintsToFillSuperview(view: collectionView)

        NSLayoutConstraint.activeConstraintsToCenterInSuperview(view: errorStack)
        NSLayoutConstraint.activeConstraintsToContainInSuperview(view: errorStack, minimumMargin: .margin)

        NSLayoutConstraint.activeConstraintsToCenterInSuperview(view: loadingIndicator)
        let views = [
            "stack": stack,
            "accessory": accessoryImageView,
        ]

        NSLayoutConstraint.activeConstraintsWithFormat("H:|-(margin)-[stack]-(padding)-[accessory]-(margin)-|", views: views)

        NSLayoutConstraint.activeConstraintsToVerticallyFillSuperview(view: stack, margin: .margin)
        accessoryImageView.centerYAnchor.constraint(equalTo: stack.centerYAnchor).isActive = true
        // Layout Badge Image
        wrapperView.addSubview(badgeImageView)
        badgeImageView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
        badgeImageView.widthAnchor.constraint(equalTo: badgeImageView.heightAnchor).isActive = true
        NSLayoutConstraint.activeConstraintsToCenterInSuperview(view: badgeImageView)

        // Layout Badge Stack
        addSubview(badgeStack)
        NSLayoutConstraint.activeConstraintsToVerticallyFillSuperview(view: badgeStack)
        badgeStack.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true

        // Ensures that the right and left line views don't surpass 400
        // but will be contained appropriately if screen width is < 400
        NSLayoutConstraint.activeConstraintsToContainHorizontallyInSuperview(view: badgeStack)
        badgeStack.widthAnchor.constraint(equalToConstant: 400).isActive = true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment