Last active
October 8, 2019 22:22
-
-
Save mikezs/63caba8127c1c586071033f09bed7e8b to your computer and use it in GitHub Desktop.
UIView+AutoLayoutAnchors.swift
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 | |
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) | |
} | |
} | |
} |
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
// 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