// // UIView.swift // VDS // // Created by Matt Bruce on 11/17/22. // import Foundation import UIKit import VDSFormControlsTokens extension UIView { public enum ConstraintIdentifier: CustomStringConvertible { case leading, trailing, top, bottom, height, width case custom(String) public var description: String { switch self { case .leading: return "leading" case .trailing: return "trailing" case .top: return "top" case .bottom: return "bottom" case .height: return "height" case .width: return "width" case .custom(let identifier): return identifier } } } public enum ConstraintType { case equal, lessThanOrEqual, greaterThanOrEqual } public var _topConstraint: NSLayoutConstraint? { constraint(with: .top)} public var _leadingConstraint: NSLayoutConstraint? { constraint(with: .leading)} public var _trailingConstraint: NSLayoutConstraint? { constraint(with: .trailing)} public var _bottomConstraint: NSLayoutConstraint? { constraint(with: .bottom)} public var _widthConstraint: NSLayoutConstraint? { constraint(with: .width)} public var _heightConstraint: NSLayoutConstraint? { constraint(with: .height)} public func constraint(with identifier: ConstraintIdentifier) -> NSLayoutConstraint? { return constraint(with: identifier.description) } public func constraint(with identifier: String) -> NSLayoutConstraint? { return constraints.first { $0.identifier == identifier } } public func removeConstraint(edges: [UIRectEdge]) { let topConstraint: NSLayoutConstraint? = constraint(with: .top) let leadingConstraint: NSLayoutConstraint? = constraint(with: .leading) let trailingConstraint: NSLayoutConstraint? = constraint(with: .trailing) let bottomConstraint: NSLayoutConstraint? = constraint(with: .bottom) edges.forEach { edge in switch edge { case .all: if let leadingConstraint { removeConstraint(leadingConstraint) } if let trailingConstraint { removeConstraint(trailingConstraint) } if let topConstraint { removeConstraint(topConstraint) } if let bottomConstraint { removeConstraint(bottomConstraint) } case .left: if let leadingConstraint { removeConstraint(leadingConstraint) } case .right: if let trailingConstraint { removeConstraint(trailingConstraint) } case .top: if let topConstraint { removeConstraint(topConstraint) } case .bottom: if let bottomConstraint { removeConstraint(bottomConstraint) } default: break } } } } //-------------------------------------------------- // MARK: - Pinning //-------------------------------------------------- extension UIView { public func pin(_ view: UIView, with edges: UIEdgeInsets = UIEdgeInsets.zero) { leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: edges.left, identifier: ConstraintIdentifier.leading.description).isActive = true trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -edges.right, identifier: ConstraintIdentifier.trailing.description).isActive = true topAnchor.constraint(equalTo: view.topAnchor, constant: edges.top, identifier: ConstraintIdentifier.top.description).isActive = true bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -edges.bottom, identifier: ConstraintIdentifier.bottom.description).isActive = true } public func pinToSuperView(_ edges: UIEdgeInsets = UIEdgeInsets.zero) { if let superview { pin(superview, with: edges) } } } //-------------------------------------------------- // MARK: - HeightAnchor //-------------------------------------------------- extension UIView { @discardableResult public func height(_ constant: CGFloat, type: ConstraintType = .equal) -> Self { switch type { case .equal: height(constant) case .lessThanOrEqual: heightLessThanEqualTo(constant) case .greaterThanOrEqual: heightGreaterThanEqualTo(constant) } return self } @discardableResult public func height(_ constant: CGFloat) -> Self { heightAnchor.constraint(equalToConstant: constant, identifier: ConstraintIdentifier.height.description).isActive = true return self } @discardableResult public func heightGreaterThanEqualTo(_ constant: CGFloat) -> Self { heightAnchor.constraint(greaterThanOrEqualToConstant: constant, identifier: ConstraintIdentifier.height.description).isActive = true return self } @discardableResult public func heightLessThanEqualTo(_ constant: CGFloat) -> Self { heightAnchor.constraint(lessThanOrEqualToConstant: constant, identifier: ConstraintIdentifier.height.description).isActive = true return self } } //-------------------------------------------------- // MARK: - WidthAnchor //-------------------------------------------------- extension UIView { @discardableResult public func width(_ constant: CGFloat, type: ConstraintType = .equal) -> Self { switch type { case .equal: width(constant) case .lessThanOrEqual: widthLessThanEqualTo(constant) case .greaterThanOrEqual: widthGreaterThanEqualTo(constant) } return self } @discardableResult public func width(_ constant: CGFloat) -> Self { widthAnchor.constraint(equalToConstant: constant, identifier: ConstraintIdentifier.width.description).isActive = true return self } @discardableResult public func widthGreaterThanEqualTo(_ constant: CGFloat) -> Self { widthAnchor.constraint(greaterThanOrEqualToConstant: constant, identifier: ConstraintIdentifier.width.description).isActive = true return self } @discardableResult public func widthLessThanEqualTo(_ constant: CGFloat) -> Self { widthAnchor.constraint(lessThanOrEqualToConstant: constant, identifier: ConstraintIdentifier.width.description).isActive = true return self } } //-------------------------------------------------- // MARK: - TopAnchor //-------------------------------------------------- extension UIView { @discardableResult public func pinTop(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ type: ConstraintType = .equal) -> Self { switch type { case .equal: pinTop(anchor, constant) case .lessThanOrEqual: pinTopLessThanOrEqualTo(anchor, constant) case .greaterThanOrEqual: pinTopGreaterThanOrEqualTo(anchor, constant) } return self } @discardableResult public func pinTop(_ constant: CGFloat = 0.0) -> Self { return pinTop(nil, constant) } @discardableResult public func pinTop(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutYAxisAnchor? = anchor ?? superview?.topAnchor if let found { topAnchor.constraint(equalTo: found, constant: constant, identifier: ConstraintIdentifier.top.description).isActive = true } return self } @discardableResult public func pinTopLessThanOrEqualTo(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutYAxisAnchor? = anchor ?? superview?.topAnchor if let found { topAnchor.constraint(lessThanOrEqualTo: found, constant: constant, identifier: ConstraintIdentifier.top.description).isActive = true } return self } @discardableResult public func pinTopGreaterThanOrEqualTo(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutYAxisAnchor? = anchor ?? superview?.topAnchor if let found { topAnchor.constraint(greaterThanOrEqualTo: found, constant: constant, identifier: ConstraintIdentifier.top.description).isActive = true } return self } } //-------------------------------------------------- // MARK: - BottomAnchor //-------------------------------------------------- extension UIView { @discardableResult public func pinBottom(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ type: ConstraintType = .equal) -> Self { switch type { case .equal: pinBottom(anchor, constant) case .lessThanOrEqual: pinBottomLessThanOrEqualTo(anchor, constant) case .greaterThanOrEqual: pinBottomGreaterThanOrEqualTo(anchor, constant) } return self } @discardableResult public func pinBottom(_ constant: CGFloat = 0.0) -> Self { return pinBottom(nil, constant) } @discardableResult public func pinBottom(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutYAxisAnchor? = anchor ?? superview?.bottomAnchor if let found { bottomAnchor.constraint(equalTo: found, constant: -constant, identifier: ConstraintIdentifier.bottom.description).isActive = true } return self } @discardableResult public func pinBottomLessThanOrEqualTo(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutYAxisAnchor? = anchor ?? superview?.bottomAnchor if let found { bottomAnchor.constraint(lessThanOrEqualTo: found, constant: -constant, identifier: ConstraintIdentifier.bottom.description).isActive = true } return self } @discardableResult public func pinBottomGreaterThanOrEqualTo(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutYAxisAnchor? = anchor ?? superview?.bottomAnchor if let found { bottomAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant, identifier: ConstraintIdentifier.bottom.description).isActive = true } return self } } //-------------------------------------------------- // MARK: - LeadingAnchor //-------------------------------------------------- extension UIView { @discardableResult public func pinLeading(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ type: ConstraintType = .equal) -> Self { switch type { case .equal: pinLeading(anchor, constant) case .lessThanOrEqual: pinLeadingLessThanOrEqualTo(anchor, constant) case .greaterThanOrEqual: pinLeadingGreaterThanOrEqualTo(anchor, constant) } return self } @discardableResult public func pinLeading(_ constant: CGFloat = 0.0) -> Self { return pinLeading(nil, constant) } @discardableResult public func pinLeading(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutXAxisAnchor? = anchor ?? superview?.leadingAnchor if let found { leadingAnchor.constraint(equalTo: found, constant: constant, identifier: ConstraintIdentifier.leading.description).isActive = true } return self } @discardableResult public func pinLeadingLessThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutXAxisAnchor? = anchor ?? superview?.leadingAnchor if let found { leadingAnchor.constraint(lessThanOrEqualTo: found, constant: constant, identifier: ConstraintIdentifier.leading.description).isActive = true } return self } @discardableResult public func pinLeadingGreaterThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutXAxisAnchor? = anchor ?? superview?.leadingAnchor if let found { leadingAnchor.constraint(greaterThanOrEqualTo: found, constant: constant, identifier: ConstraintIdentifier.leading.description).isActive = true } return self } } //-------------------------------------------------- // MARK: - TrailingAnchor //-------------------------------------------------- extension UIView { @discardableResult public func pinTrailing(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ type: ConstraintType = .equal) -> Self { switch type { case .equal: pinTrailing(anchor, constant) case .lessThanOrEqual: pinTrailingLessThanOrEqualTo(anchor, constant) case .greaterThanOrEqual: pinLeadingGreaterThanOrEqualTo(anchor, constant) } return self } @discardableResult public func pinTrailing(_ constant: CGFloat = 0.0) -> Self { return pinTrailing(nil, constant) } @discardableResult public func pinTrailing(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutXAxisAnchor? = anchor ?? superview?.trailingAnchor if let found { trailingAnchor.constraint(equalTo: found, constant: -constant, identifier: ConstraintIdentifier.trailing.description).isActive = true } return self } @discardableResult public func pinTrailingLessThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutXAxisAnchor? = anchor ?? superview?.trailingAnchor if let found { trailingAnchor.constraint(lessThanOrEqualTo: found, constant: -constant, identifier: ConstraintIdentifier.trailing.description).isActive = true } return self } @discardableResult public func pinTrailingGreaterThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { let found: NSLayoutXAxisAnchor? = anchor ?? superview?.trailingAnchor if let found { trailingAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant, identifier: ConstraintIdentifier.trailing.description).isActive = true } return self } } //-------------------------------------------------- // MARK: - Debug Borders //-------------------------------------------------- extension UIView { internal func removeDebugBorder() { layer.remove(layerName: "debug") } internal func addDebugBorder(color: UIColor = .red) { //ensure you remove existing removeDebugBorder() //add bounds border let borderLayer = CALayer() borderLayer.name = "debugAreaLayer" borderLayer.frame = bounds borderLayer.bounds = bounds borderLayer.borderWidth = VDSFormControls.widthBorder borderLayer.borderColor = color.cgColor layer.addSublayer(borderLayer) //add touchborder if applicable if type(of: self) is AppleGuidlinesTouchable.Type { let faultToleranceX: CGFloat = max((45 - bounds.size.width) / 2.0, 0) let faultToleranceY: CGFloat = max((45 - bounds.size.height) / 2.0, 0) let touchableAreaPath = UIBezierPath(rect: bounds.insetBy(dx: -faultToleranceX, dy: -faultToleranceY)) let touchLayer = CAShapeLayer() touchLayer.path = touchableAreaPath.cgPath touchLayer.strokeColor = color.cgColor touchLayer.fillColor = UIColor.clear.cgColor touchLayer.lineWidth = VDSFormControls.widthBorder touchLayer.opacity = 1.0 touchLayer.name = "debugTouchableAreaLayer" touchLayer.zPosition = 100 touchLayer.frame = bounds touchLayer.bounds = bounds layer.addSublayer(touchLayer) } } public var hasDebugBorder: Bool { guard let layers = layer.sublayers else { return false } return layers.compactMap{$0.name}.filter{$0.hasPrefix("debug")}.count > 0 } public func debugBorder(show shouldShow: Bool = true, color: UIColor = .red) { if shouldShow { addDebugBorder(color: color) } else { removeDebugBorder() } if let view = self as? Handlerable { view.updateView() } } } //-------------------------------------------------- // MARK: - CALayer //-------------------------------------------------- extension CALayer { func remove(layerName: String) { guard let sublayers = sublayers else { return } sublayers.forEach({ layer in if layer.name?.hasPrefix(layerName) ?? false { layer.removeFromSuperlayer() } }) } } //-------------------------------------------------- // MARK: - Borders //-------------------------------------------------- extension UIView { public func addBorder(side: UIRectEdge, width: CGFloat, color: UIColor, offset: CGFloat = 0) { let layerName = borderLayerName(for: side) layer.remove(layerName: layerName) let borderLayer = CALayer() borderLayer.backgroundColor = color.cgColor borderLayer.name = layerName switch side { case .left: borderLayer.frame = CGRect(x: 0, y: 0, width: width, height: frame.height) case .right: borderLayer.frame = CGRect(x: frame.width - width - offset, y: 0, width: width, height: frame.height) case .top: borderLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: width) case .bottom: borderLayer.frame = CGRect(x: 0, y: frame.height - width - offset, width: frame.width, height: width) default: break } layer.addSublayer(borderLayer) } public func removeBorders() { layer.borderWidth = 0 layer.borderColor = nil layer.remove(layerName: borderLayerName(for: .top)) layer.remove(layerName: borderLayerName(for: .left)) layer.remove(layerName: borderLayerName(for: .right)) layer.remove(layerName: borderLayerName(for: .bottom)) } private func borderLayerName(for side: UIRectEdge) -> String { switch side { case .left: return "leftBorderLayer" case .right: return "rightBorderLayer" case .top: return "topBorderLayer" case .bottom: return "bottomBorderLayer" default: return "" } } } //-------------------------------------------------- // MARK: - NSLayoutAnchor //-------------------------------------------------- extension NSLayoutAnchor { // These methods return an inactive constraint of the form thisAnchor = otherAnchor. @discardableResult @objc public func constraint(equalTo anchor: NSLayoutAnchor, identifier: String) -> NSLayoutConstraint { let constraint = self.constraint(equalTo: anchor) constraint.identifier = identifier return constraint } @discardableResult @objc public func constraint(greaterThanOrEqualTo anchor: NSLayoutAnchor, identifier: String) -> NSLayoutConstraint { let constraint = self.constraint(greaterThanOrEqualTo: anchor) constraint.identifier = identifier return constraint } @discardableResult @objc public func constraint(lessThanOrEqualTo anchor: NSLayoutAnchor, identifier: String) -> NSLayoutConstraint { let constraint = self.constraint(lessThanOrEqualTo: anchor) constraint.identifier = identifier return constraint } @discardableResult @objc public func constraint(equalTo anchor: NSLayoutAnchor, constant: CGFloat, identifier: String) -> NSLayoutConstraint { let constraint = self.constraint(equalTo: anchor, constant: constant) constraint.identifier = identifier return constraint } @discardableResult @objc public func constraint(greaterThanOrEqualTo anchor: NSLayoutAnchor, constant: CGFloat, identifier: String) -> NSLayoutConstraint { let constraint = self.constraint(greaterThanOrEqualTo: anchor, constant: constant) constraint.identifier = identifier return constraint } @discardableResult @objc public func constraint(lessThanOrEqualTo anchor: NSLayoutAnchor, constant: CGFloat, identifier: String) -> NSLayoutConstraint { let constraint = self.constraint(lessThanOrEqualTo: anchor, constant: constant) constraint.identifier = identifier return constraint } } //-------------------------------------------------- // MARK: - NSLayoutDimension //-------------------------------------------------- extension NSLayoutDimension { // These methods return an inactive constraint of the form thisVariable = constant. @discardableResult @objc public func constraint(equalToConstant c: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(equalToConstant: c) lc.identifier = identifier return lc } @discardableResult @objc public func constraint(greaterThanOrEqualToConstant c: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(greaterThanOrEqualToConstant: c) lc.identifier = identifier return lc } @discardableResult @objc public func constraint(lessThanOrEqualToConstant c: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(lessThanOrEqualToConstant: c) lc.identifier = identifier return lc } // These methods return an inactive constraint of the form thisAnchor = otherAnchor * multiplier. @discardableResult @objc public func constraint(equalTo anchor: NSLayoutDimension, multiplier m: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(equalTo: anchor, multiplier: m) lc.identifier = identifier return lc } @discardableResult @objc public func constraint(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(greaterThanOrEqualTo: anchor ,multiplier: m) lc.identifier = identifier return lc } @discardableResult @objc public func constraint(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(lessThanOrEqualTo: anchor, multiplier: m) lc.identifier = identifier return lc } // These methods return an inactive constraint of the form thisAnchor = otherAnchor * multiplier + constant. @discardableResult @objc public func constraint(equalTo anchor: NSLayoutDimension, multiplier m: CGFloat, constant c: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(equalTo: anchor, multiplier: m, constant: c) lc.identifier = identifier return lc } @discardableResult @objc public func constraint(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat, constant c: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(greaterThanOrEqualTo: anchor, multiplier: m, constant: c) lc.identifier = identifier return lc } @discardableResult @objc public func constraint(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat, constant c: CGFloat, identifier: String) -> NSLayoutConstraint { let lc = constraint(lessThanOrEqualTo: anchor, multiplier: m, constant: c) lc.identifier = identifier return lc } }