Skip to content

Instantly share code, notes, and snippets.

@mikezs
Last active October 8, 2019 22:22
Show Gist options
  • Save mikezs/63caba8127c1c586071033f09bed7e8b to your computer and use it in GitHub Desktop.
Save mikezs/63caba8127c1c586071033f09bed7e8b to your computer and use it in GitHub Desktop.
UIView+AutoLayoutAnchors.swift
import UIKit
protocol LayoutAttributeConvertible {
var layoutAttribute: NSLayoutAttribute { get }
}
enum Edge: LayoutAttributeConvertible {
case left
case right
case top
case bottom
case leading
case trailing
var layoutAttribute: NSLayoutAttribute {
switch self {
case .left:
return .left
case .right:
return .right
case .top:
return .top
case .bottom:
return .bottom
case .leading:
return .leading
case .trailing:
return .trailing
}
}
static func edgeForAttribute(_ layoutAttribute: NSLayoutAttribute) -> Edge? {
switch layoutAttribute {
case .left:
return .left
case .right:
return .right
case .top:
return .top
case .bottom:
return .bottom
case .leading:
return .leading
case .trailing:
return .trailing
default:
return nil
}
}
}
enum Dimension: LayoutAttributeConvertible {
case width
case height
case noDimension
var layoutAttribute: NSLayoutAttribute {
switch self {
case .width:
return .width
case .height:
return .height
case .noDimension:
return .notAnAttribute
}
}
}
enum Axis: LayoutAttributeConvertible {
case horizontal
case vertical
var layoutAttribute: NSLayoutAttribute {
switch self {
case .horizontal: return .centerY
case .vertical: return .centerX
}
}
}
extension UIView {
// MARK: - Superview target
/**
Centers the view in its superview.
- parameter withHorizontalOffset: The horizontal offset from the centre of the superview.
- parameter verticalOffset: The veritical offset from the centre of the superview.
- returns: An array of constraints added.
*/
@discardableResult func autoCenterInSuperview(withHorizontalOffset horizontalOffset: CGFloat = 0.0, verticalOffset: CGFloat = 0.0) -> [NSLayoutConstraint] {
var constraints = [NSLayoutConstraint]()
for axis: Axis in [.horizontal, .vertical] {
constraints.append(self.autoAlignAxisToSuperviewAxis(axis, withOffset: axis == .vertical ? verticalOffset : horizontalOffset))
}
return constraints
}
/**
Aligns the view to the same axis of its superview.
- parameter axis: The axis of this view and of its superview to align.
- parameter withOffset: The offset between the axis of this view and the axis of the other view.
- returns: The constraint added.
*/
@discardableResult func autoAlignAxisToSuperviewAxis(_ axis: Axis, withOffset offset: CGFloat = 0.0) -> NSLayoutConstraint {
assert(self.superview != nil)
return self.autoAlignAxis(axis, toSameAxisOfView: self.superview!, withOffset: offset)
}
/**
Aligns an edge of the view to the axis of the superview with an offset.
- parameter edge: The edge of this view to align.
- parameter toAxisOfSuperview: The axis of the other view to align to.
- parameter withOffset: The offset between the axis of this view and the axis of the other view.
- parameter activate: Whether or not to activate the constraint automatically when it is created.
- returns: The constraint added.
*/
@discardableResult func autoAlignEdge(_ edge: Edge, toAxisOfSuperview axis: Axis, withOffset offset: CGFloat = 0.0, activate: Bool = true) -> NSLayoutConstraint {
assert(self.superview != nil)
return self.autoConstrainAttribute(edge, toAttribute: axis, ofView: self.superview!, withMultiplier: 1.0, withAmount: offset, activate: activate)
}
// MARK: - Pin Edges to Superview
/**
Pins the given edge of the view to the same edge of its superview with an optional inset as a maximum or minimum.
- parameter edge: The edge of this view and its superview to pin.
- parameter withInset: The amount to inset this view's edge from the superview's edge.
- parameter relation: Whether the inset should be at least, at most, or exactly equal to the given value.
- parameter priority: Priority of the constraint, defaults to Required.
- returns: The constraint added.
*/
@discardableResult func autoPinEdgeToSuperviewEdge(_ edge: Edge, withInset inset: CGFloat = 0.0, relation: NSLayoutRelation = .equal, priority: UILayoutPriority = .required) -> NSLayoutConstraint {
assert(self.superview != nil)
var correctedRelation = relation
var correctedInset = inset
// The bottom, right, and trailing insets (and relations, if an inequality) are inverted to become offsets
if [.bottom, .right, .trailing].contains(edge) {
correctedInset = -inset
switch relation {
case .greaterThanOrEqual:
correctedRelation = .lessThanOrEqual
case .lessThanOrEqual:
correctedRelation = .greaterThanOrEqual
default:
break
}
}
return self.autoPinEdge(edge, toEdge: edge, ofView: self.superview!, withOffset: correctedInset, relation: correctedRelation, priority: priority)
}
/**
Pins the 4 edges of the view to the edges of its superview with the given edge insets, excluding any edges provided.
The insets.left corresponds to a leading edge constraint, and insets.right corresponds to a trailing edge constraint.
- parameter withInsets: The insets for this view's edges from its superview's edges. The inset corresponding to the excluded edge
will be ignored.
- parameter excludingEdges: The edges of this view to exclude in pinning to its superview; this method will not apply any constraint to it.
- parameter priority: Priority of the constraint, defaults to Required.
- returns: An array of constraints added.
*/
@discardableResult func autoPinEdgesToSuperviewEdges(withInsets insets: UIEdgeInsets = UIEdgeInsets.zero, excludingEdges: [Edge]? = nil, priority: UILayoutPriority = .required) -> [NSLayoutConstraint] {
var constraints = [NSLayoutConstraint]()
if excludingEdges == nil || !excludingEdges!.contains(.top) {
constraints.append(self.autoPinEdgeToSuperviewEdge(.top, withInset: insets.top, priority: priority))
}
if excludingEdges == nil || (!excludingEdges!.contains(.leading) && !excludingEdges!.contains(.left)) {
constraints.append(self.autoPinEdgeToSuperviewEdge(.leading, withInset: insets.left, priority: priority))
}
if excludingEdges == nil || !excludingEdges!.contains(.bottom) {
constraints.append(self.autoPinEdgeToSuperviewEdge(.bottom, withInset: insets.bottom, priority: priority))
}
if excludingEdges == nil || (!excludingEdges!.contains(.trailing) && !excludingEdges!.contains(.right)) {
constraints.append(self.autoPinEdgeToSuperviewEdge(.trailing, withInset: insets.right, priority: priority))
}
return constraints
}
// MARK: - Pin Edges
/**
Pins an edge of the view to a given edge of another view with an offset as a maximum or minimum.
- parameter edge: The edge of this view to pin.
- parameter toEdge: The edge of the other view to pin to.
- parameter ofView: The other view to pin to. Must be in the same view hierarchy as this view.
- parameter withOffset: The offset between the edge of this view and the edge of the other view.
- parameter relation: Whether the offset should be at least, at most, or exactly equal to the given value.
- parameter priority: Priority of the constraint, defaults to Required.
- returns: The constraint added.
*/
@discardableResult func autoPinEdge(_ edge: Edge, toEdge: Edge, ofView: UIView, withOffset offset: CGFloat = 0.0, relation: NSLayoutRelation = .equal, priority: UILayoutPriority = .required) -> NSLayoutConstraint {
return self.autoConstrainAttribute(edge, toAttribute: toEdge, ofView: ofView, withMultiplier: 1.0, withAmount: offset, relation: relation, priority: priority)
}
// MARK: - Align Axes
/**
Aligns an axis of the view to the same axis of another view with an offset.
- parameter axis: The axis of this view and the other view to align.
- parameter toSameAxisOfView: The other view to align to. Must be in the same view hierarchy as this view.
- parameter withOffset: The offset between the axis of this view and the axis of the other view.
- returns: The constraint added.
*/
@discardableResult func autoAlignAxis(_ axis: Axis, toSameAxisOfView ofView: UIView, withOffset offset: CGFloat = 0.0) -> NSLayoutConstraint {
return self.autoConstrainAttribute(axis, toAttribute: axis, ofView: ofView, withMultiplier: 1.0, withAmount: offset)
}
/**
Aligns an axis of the view to the same axis of another view with a multiplier.
- parameter axis: The axis of this view and the other view to align.
- parameter toSameAxisOfView: The other view to align to. Must be in the same view hierarchy as this view.
- parameter withMultiplier: The multiplier between the axis of this view and the axis of the other view.
- returns: The constraint added.
*/
@discardableResult func autoAlignAxis(_ axis: Axis, toSameAxisOfView ofView: UIView, withMultiplier multiplier: CGFloat) -> NSLayoutConstraint {
return self.autoConstrainAttribute(axis, toAttribute: axis, ofView: ofView, withMultiplier: multiplier)
}
// MARK: - Align Edge
/**
Aligns an edge of the view to an axis of another view with an offset.
- parameter edge: The edge of this view to align.
- parameter toAxis: The axis of the other view to align to.
- parameter ofView: The other view to align to. Must be in the same view hierarchy as this view.
- parameter withOffset: The offset between the axis of this view and the axis of the other view.
- returns: The constraint added.
*/
@discardableResult func autoAlignEdge(_ edge: Edge, toAxis: Axis, ofView: UIView, withOffset offset: CGFloat = 0.0) -> NSLayoutConstraint {
return self.autoConstrainAttribute(edge, toAttribute: toAxis, ofView: ofView, withMultiplier: 1.0, withAmount: offset)
}
// MARK: - Match Dimensions
/**
Matches a dimension of the view to a given dimension of another view.
- parameter dimension: The dimension of this view to pin.
- parameter toDimension: The dimension of the other view to pin to.
- parameter ofView: The other view to match to. Must be in the same view hierarchy as this view.
- returns: The constraint added.
*/
@discardableResult func autoMatchDimension(_ dimension: Dimension, toDimension: Dimension, ofView: UIView) -> NSLayoutConstraint {
return self.autoConstrainAttribute(dimension, toAttribute: toDimension, ofView: ofView)
}
/**
Matches a dimension of the view to a multiple of a given dimension of another view as a maximum or minimum.
- parameter dimension: The dimension of this view to pin.
- parameter toDimension: The dimension of the other view to pin to.
- parameter ofView: The other view to match to. Must be in the same view hierarchy as this view.
- parameter withMultiplier: The multiple of the other view's given dimension that this view's given dimension should be.
- parameter andOffset: The offset between the dimension of this view and the dimension of the other view.
- parameter relation: Whether the multiple should be at least, at most, or exactly equal to the given value.
- returns: The constraint added.
*/
@discardableResult func autoMatchDimension(_ dimension: Dimension, toDimension: Dimension, ofView: UIView, withMultiplier: CGFloat, andOffset offset: CGFloat = 0.0, relation: NSLayoutRelation = .equal) -> NSLayoutConstraint {
return self.autoConstrainAttribute(dimension, toAttribute: toDimension, ofView: ofView, withMultiplier: withMultiplier, withAmount: offset, relation: relation)
}
/**
Matches a dimension of the view to a given dimension of another view with an offset as a maximum or minimum.
- parameter dimension: The dimension of this view to pin.
- parameter toDimension: The dimension of the other view to pin to.
- parameter ofView: The other view to match to. Must be in the same view hierarchy as this view.
- parameter withOffset: The offset between the dimension of this view and the dimension of the other view.
- parameter relation: Whether the multiple should be at least, at most, or exactly equal to the given value.
- returns: The constraint added.
*/
@discardableResult func autoMatchDimension(_ dimension: Dimension, toDimension: Dimension, ofView: UIView, withOffset: CGFloat, relation: NSLayoutRelation = .equal) -> NSLayoutConstraint {
return self.autoConstrainAttribute(dimension, toAttribute: toDimension, ofView: ofView, withMultiplier: 1.0, withAmount: withOffset, relation: relation)
}
// MARK: - Set Dimensions
/**
Sets the view to a specific size.
- parameter size: The size to set this view's dimensions to.
- parameter activate: Wether or not to activate the constraint after it's created. Defaults to true
- returns: An array of constraints added.
*/
@discardableResult func autoSetDimensionsToSize(_ size: CGFloat, activate: Bool = true) -> [NSLayoutConstraint] {
var constraints = [NSLayoutConstraint]()
for dimension: Dimension in [.width, .height] {
constraints.append(self.autoSetDimension(dimension, toSize: size, activate: activate))
}
return constraints
}
/**
Sets the given dimension of the view to a specific size as a maximum or minimum.
- parameter dimension: The dimension of this view to set.
- parameter toSize: The size to set the given dimension to.
- parameter relation: Whether the size should be at least, at most, or exactly equal to the given value.
- parameter activate: Wether or not to activate the constraint after it's created. Defaults to true
- parameter priority: Priority of the constraint, defaults to Required.
- returns: The constraint added.
*/
@discardableResult func autoSetDimension(_ dimension: Dimension, toSize size: CGFloat, relation: NSLayoutRelation = .equal, activate: Bool = true, priority: UILayoutPriority = .required) -> NSLayoutConstraint {
return self.autoConstrainAttribute(dimension, toAttribute: Dimension.noDimension, ofView: nil, withMultiplier: 1.0, withAmount: size, relation: relation, activate: activate, priority: priority)
}
// MARK: - Constrain Any Attributes
/**
Constrains an attribute of the view to a given attribute of another view with an offset as a maximum or minimum.
This method can be used to constrain different types of attributes across two views.
- parameter attribute: Any attribute of this view to constrain.
- parameter toAttribute: Any attribute of the other view to constrain to.
- parameter ofView: The other view to constrain to. Must be in the same view hierarchy as this view.
- parameter withMultiplier: The multiplier between the attribute of this view and the attribute of the other view.
- parameter withAmount: The offset between the attribute of this view and the attribute of the other view.
- parameter relation: Whether the offset should be at least, at most, or exactly equal to the given value.
- parameter activate: Wether or not to activate the constraint after it's created. Defaults to true
- parameter priority: Priority of the constraint, defaults to Required.
- returns: The constraint added.
*/
@discardableResult func autoConstrainAttribute(_ attribute: LayoutAttributeConvertible, toAttribute: LayoutAttributeConvertible, ofView: UIView? = nil, withMultiplier: CGFloat = 1.0, withAmount: CGFloat = 0.0, relation: NSLayoutRelation = .equal, activate: Bool = true, priority: UILayoutPriority = .required) -> NSLayoutConstraint {
self.translatesAutoresizingMaskIntoConstraints = false
let constraint: NSLayoutConstraint
var newAmount = withAmount
if #available(iOS 11, *) {
switch (attribute.layoutAttribute, toAttribute.layoutAttribute) {
case (.left, .left), (.leading, .leading):
newAmount += ofView?.safeAreaInsets.left ?? 0.0
case (.right, .right), (.trailing, .trailing):
newAmount -= ofView?.safeAreaInsets.right ?? 0.0
case (.top, .top):
newAmount += ofView?.safeAreaInsets.top ?? 0.0
case (.bottom, .bottom):
newAmount -= ofView?.safeAreaInsets.bottom ?? 0.0
default:
()
}
}
constraint = NSLayoutConstraint(item: self, attribute: attribute.layoutAttribute, relatedBy: relation, toItem: ofView, attribute: toAttribute.layoutAttribute, multiplier: withMultiplier, constant: newAmount)
constraint.priority = priority
if activate {
constraint.isActive = true
}
return constraint
}
}
extension UIView {
/**
Find the constraints for a given edge
- note: searches both the current view and the superview
- parameter edge: the edge to find constraints for
- returns: constraints for the given edge
*/
@discardableResult func constraintsForEdge(_ edge: Edge) -> [NSLayoutConstraint] {
var constraints = [NSLayoutConstraint]()
var subjects = self.constraints
if let superviewConstraints = self.superview?.constraints {
subjects.append(contentsOf: superviewConstraints)
}
for constraint in subjects {
if let item = constraint.firstItem as? UIView, item == self {
if Edge.edgeForAttribute(constraint.firstAttribute) == edge {
constraints.append(constraint)
}
} else if let item = constraint.secondItem as? UIView, item == self {
if Edge.edgeForAttribute(constraint.secondAttribute) == edge {
constraints.append(constraint)
}
}
}
return constraints
}
/**
Find a constraint for a given edge
- note: searches both the current view and the superview
- parameter edge: the edge to find a constraint for
- returns: the first constraint for the given edge
*/
@discardableResult func constraintForEdge(_ edge: Edge) -> NSLayoutConstraint? {
return self.constraintsForEdge(edge).first
}
}
extension Array where Element: NSLayoutConstraint {
/**
Set all the constraints in this array active or inactive
- parameter active: flag to set if the constraints should be active or not
*/
func setActive(_ active: Bool) {
for element in self {
element.isActive = active
}
}
}
extension Array where Element: UIView {
/**
Auto align the edge of every element to the first element
- parameter edge: the edge to align
*/
func autoAlignToEdge(_ edge: Edge) {
guard self.count >= 2 else { return }
let firstView = self.first!
for (index, view) in self.enumerated() where index > 0 {
view.autoPinEdge(edge, toEdge: edge, ofView: firstView)
}
}
}
// MARK: - Autolayout
extension UIView
{
func anchor(for attribute: NSLayoutAttribute) -> NSLayoutXAxisAnchor?
{
switch attribute
{
case .left: return self.leftAnchor
case .leading: return self.leadingAnchor
case .right: return self.rightAnchor
case .trailing: return self.trailingAnchor
case .centerX: return self.centerXAnchor
default: return nil
}
}
func anchor(for attribute: NSLayoutAttribute) -> NSLayoutYAxisAnchor?
{
switch attribute
{
case .top, .topMargin: return self.topAnchor
case .bottom, .bottomMargin: return self.bottomAnchor
case .centerY: return self.centerYAnchor
default: return nil
}
}
func anchor(for attribute: NSLayoutAttribute) -> NSLayoutDimension?
{
switch attribute
{
case .width: return self.widthAnchor
case .height: return self.heightAnchor
default: return nil
}
}
@discardableResult
func anchor(_ attribute: NSLayoutAttribute, toSuperviews otherAttribute: NSLayoutAttribute, constant: CGFloat = 0) -> NSLayoutConstraint?
{
guard let superview = self.superview else {
assertionFailure("View has no superview")
return nil
}
return self.anchor(attribute, to: otherAttribute, of: superview)
}
@discardableResult
func anchor(_ attribute: NSLayoutAttribute, to otherAttribute: NSLayoutAttribute, of view: UIView, constant: CGFloat = 0) -> NSLayoutConstraint?
{
self.translatesAutoresizingMaskIntoConstraints = false
if let xAxisAnchor = self.anchor(for: attribute) as NSLayoutXAxisAnchor?, let otherXAxisAnchor = self.anchor(for: otherAttribute) as NSLayoutXAxisAnchor?
{
return xAxisAnchor.constraint(equalTo: otherXAxisAnchor, constant: constant)
}
else if let yAxisAnchor = self.anchor(for: attribute) as NSLayoutYAxisAnchor?, let otherYAxisAnchor = self.anchor(for: otherAttribute) as NSLayoutYAxisAnchor?
{
return yAxisAnchor.constraint(equalTo: otherYAxisAnchor, constant: constant)
}
else if let edgeDimension = self.anchor(for: attribute) as NSLayoutDimension?, let otherDimension = self.anchor(for: otherAttribute) as NSLayoutDimension?
{
return edgeDimension.constraint(equalTo: otherDimension, constant: constant)
}
assertionFailure("Tried to constrain mismatching attributes")
return nil
}
@discardableResult
func anchorEdgesToSuperviewEdges(insets: UIEdgeInsets = .zero, excluding: [NSLayoutAttribute] = []) -> [NSLayoutAttribute: NSLayoutConstraint]
{
// TODO: Make sure this assert works, it's meant to test it only contains these
//assert(excluding.contains(where: { ![.left, .right, .leading, .trailing, .top, .bottom].contains($0) }))
guard let superview = self.superview else { assertionFailure("View has no superview"); return [:] }
self.translatesAutoresizingMaskIntoConstraints = false
var constraints = [NSLayoutAttribute: NSLayoutConstraint]()
if !(excluding.contains(.left) || excluding.contains(.leading))
{
constraints[.left] = self.leftAnchor.constraint(equalTo: superview.leftAnchor, constant: insets.left)
}
if !(excluding.contains(.right) || excluding.contains(.trailing))
{
constraints[.right] = self.rightAnchor.constraint(equalTo: superview.rightAnchor, constant: insets.right)
}
if !excluding.contains(.top)
{
constraints[.top] = self.topAnchor.constraint(equalTo: superview.topAnchor, constant: insets.top)
}
if !excluding.contains(.bottom)
{
constraints[.bottom] = self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: insets.bottom)
}
NSLayoutConstraint.activate(Array(constraints.values))
return constraints
}
@discardableResult
func anchorCenterInSuperview(offset: CGPoint = .zero) -> [NSLayoutAttribute: NSLayoutConstraint]
{
guard let superview = self.superview else { assertionFailure("View has no superview"); return [:] }
self.translatesAutoresizingMaskIntoConstraints = false
let constraints: [NSLayoutAttribute: NSLayoutConstraint] = [
.centerX: self.centerXAnchor.constraint(equalTo: superview.centerXAnchor, constant: offset.x),
.centerY: self.centerYAnchor.constraint(equalTo: superview.centerYAnchor, constant: offset.y)
]
NSLayoutConstraint.activate(Array(constraints.values))
return constraints
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment