TM-SGNL-iOS/SignalUI/UIKitExtensions/UIView+AutoLayout.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

319 lines
12 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public import PureLayout
import SignalServiceKit
public extension UIView {
// MARK: Superview edges
@discardableResult
func autoPinEdge(toSuperviewEdge edge: ALEdge, relation: NSLayoutConstraint.Relation) -> NSLayoutConstraint {
return autoPinEdge(toSuperviewEdge: edge, withInset: 0, relation: relation)
}
@discardableResult
func autoPinEdges(toSuperviewEdgesExcludingEdge edge: ALEdge) -> [NSLayoutConstraint] {
return autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: edge)
}
// MARK: Superview safe area
@discardableResult
func autoPinEdges(toSuperviewSafeAreaExcludingEdge edge: ALEdge) -> [NSLayoutConstraint] {
return autoPinEdgesToSuperviewSafeArea(with: .zero, excludingEdge: edge)
}
// MARK: Horizontal edges to superview margins
@discardableResult
func autoPinLeadingToSuperviewMargin(withInset inset: CGFloat = 0) -> NSLayoutConstraint {
return autoPinEdge(toSuperviewMargin: .leading, withInset: inset)
}
@discardableResult
func autoPinTrailingToSuperviewMargin(withInset inset: CGFloat = 0) -> NSLayoutConstraint {
return autoPinEdge(toSuperviewMargin: .trailing, withInset: inset)
}
@discardableResult
func autoPinWidthToSuperviewMargins(withInset inset: CGFloat) -> [NSLayoutConstraint] {
return [
autoPinEdge(toSuperviewMargin: .leading, withInset: inset),
autoPinEdge(toSuperviewMargin: .trailing, withInset: inset)
]
}
@discardableResult
func autoPinWidthToSuperviewMargins(relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
// We invert the relation because of the weird grammar switch when talking about
// the size of widths to the positioning of edges
// "Width less than or equal to superview margin width"
// -> "Leading edge greater than or equal to superview leading edge"
// -> "Trailing edge less than or equal to superview trailing edge" (then PureLayout re-inverts for whatever reason)
let resolvedRelation = relation.inverse
return [
autoPinEdge(toSuperviewMargin: .leading, relation: resolvedRelation),
autoPinEdge(toSuperviewMargin: .trailing, relation: resolvedRelation)
]
}
// MARK: Vertical edges to superview margins
@discardableResult
func autoPinTopToSuperviewMargin(withInset inset: CGFloat = 0) -> NSLayoutConstraint {
return autoPinEdge(toSuperviewMargin: .top, withInset: inset)
}
@discardableResult
func autoPinBottomToSuperviewMargin(withInset inset: CGFloat = 0) -> NSLayoutConstraint {
return autoPinEdge(toSuperviewMargin: .bottom, withInset: inset)
}
@discardableResult
func autoPinHeightToSuperviewMargins(relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
// We invert the relation because of the weird grammar switch when talking about
// the size of height to the positioning of edges
// "Height less than or equal to superview margin height"
// -> "Top edge greater than or equal to superview top edge"
// -> "Bottom edge less than or equal to superview bottom edge" (then PureLayout re-inverts for whatever reason)
let resolvedRelation = relation.inverse
return [
autoPinEdge(toSuperviewMargin: .top, relation: resolvedRelation),
autoPinEdge(toSuperviewMargin: .bottom, relation: resolvedRelation)
]
}
// MARK: Width / height to superview
@discardableResult
func autoPinWidthToSuperview(withMargin margin: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
// We invert the relation because of the weird grammar switch when talking about
// the size of widths to the positioning of edges
// "Width less than or equal to superview margin width"
// -> "Leading edge greater than or equal to superview leading edge"
// -> "Trailing edge less than or equal to superview trailing edge" (then PureLayout re-inverts for whatever reason)
let resolvedRelation = relation.inverse
return [
autoPinEdge(toSuperviewEdge: .leading, withInset: margin, relation: resolvedRelation),
autoPinEdge(toSuperviewEdge: .trailing, withInset: margin, relation: resolvedRelation)
]
}
@discardableResult
func autoPinHeightToSuperview(withMargin margin: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
// We invert the relation because of the weird grammar switch when talking about
// the size of height to the positioning of edges
// "Height less than or equal to superview margin height"
// -> "Top edge greater than or equal to superview top edge"
// -> "Bottom edge less than or equal to superview bottom edge" (then PureLayout re-inverts for whatever reason)
let resolvedRelation = relation.inverse
return [
autoPinEdge(toSuperviewEdge: .top, withInset: margin, relation: resolvedRelation),
autoPinEdge(toSuperviewEdge: .bottom, withInset: margin, relation: resolvedRelation)
]
}
// MARK: Edges to another view's edges
@discardableResult
func autoPinEdges(toEdgesOf view: UIView, with insets: UIEdgeInsets = .zero) -> [NSLayoutConstraint] {
return [
autoPinEdge(.leading, to: .leading, of: view, withOffset: insets.leading),
autoPinEdge(.top, to: .top, of: view, withOffset: insets.top),
autoPinEdge(.trailing, to: .trailing, of: view, withOffset: -insets.trailing),
autoPinEdge(.bottom, to: .bottom, of: view, withOffset: -insets.bottom)
]
}
@discardableResult
func autoPinLeading(toTrailingEdgeOf view: UIView, offset: CGFloat = 0) -> NSLayoutConstraint {
autoPinEdge(.leading, to: .trailing, of: view, withOffset: offset)
}
@discardableResult
func autoPinTrailing(toLeadingEdgeOf view: UIView, offset: CGFloat = 0) -> NSLayoutConstraint {
autoPinEdge(.trailing, to: .leading, of: view, withOffset: -offset)
}
@discardableResult
func autoPinHorizontalEdges(toEdgesOf view: UIView) -> [NSLayoutConstraint] {
return [
autoPinEdge(.leading, to: .leading, of: view),
autoPinEdge(.trailing, to: .trailing, of: view)
]
}
@discardableResult
func autoPinVerticalEdges(toEdgesOf view: UIView) -> [NSLayoutConstraint] {
return [
autoPinEdge(.top, to: .top, of: view),
autoPinEdge(.bottom, to: .bottom, of: view)
]
}
@discardableResult
func autoPinLeading(toEdgeOf view: UIView, offset: CGFloat = 0) -> NSLayoutConstraint {
return autoPinEdge(.leading, to: .leading, of: view, withOffset: offset)
}
@discardableResult
func autoPinTrailing(toEdgeOf view: UIView, offset: CGFloat = 0) -> NSLayoutConstraint {
return autoPinEdge(.trailing, to: .trailing, of: view, withOffset: offset)
}
// MARK: Width & Height
@discardableResult
func autoPinHeight(toHeightOf otherView: UIView, offset: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
return autoMatch(.height, to: .height, of: otherView, withOffset: offset, relation: relation)
}
@discardableResult
func autoPinWidth(toWidthOf otherView: UIView, offset: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
return autoMatch(.width, to: .width, of: otherView, withOffset: offset, relation: relation)
}
static func matchWidthsOfViews(_ views: [UIView]) {
var firstView: UIView?
for view in views {
if let otherView = firstView {
view.autoMatch(.width, to: .width, of: otherView)
} else {
firstView = view
}
}
}
static func matchHeightsOfViews(_ views: [UIView]) {
var firstView: UIView?
for view in views {
if let otherView = firstView {
view.autoMatch(.height, to: .height, of: otherView)
} else {
firstView = view
}
}
}
// MARK: Centering
@discardableResult
func autoHCenterInSuperview() -> NSLayoutConstraint {
return autoAlignAxis(.vertical, toSameAxisOf: superview!)
}
@discardableResult
func autoVCenterInSuperview() -> NSLayoutConstraint {
return autoAlignAxis(.horizontal, toSameAxisOf: superview!)
}
// MARK: Aspect Ratio
@discardableResult
func autoPinToSquareAspectRatio() -> NSLayoutConstraint {
return autoPin(toAspectRatio: 1)
}
@discardableResult
func autoPinToAspectRatio(withSize size: CGSize) -> NSLayoutConstraint {
return autoPin(toAspectRatio: size.aspectRatio)
}
@discardableResult
func autoPin(toAspectRatio ratio: CGFloat, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
// Clamp to ensure view has reasonable aspect ratio.
let clampedRatio: CGFloat = CGFloat.clamp(ratio, min: 0.05, max: 95.0)
if clampedRatio != ratio {
owsFailDebug("Invalid aspect ratio: \(ratio) for view: \(self)")
}
translatesAutoresizingMaskIntoConstraints = false
let constraint = NSLayoutConstraint(
item: self,
attribute: .width,
relatedBy: relation,
toItem: self,
attribute: .height,
multiplier: clampedRatio,
constant: 0)
constraint.autoInstall()
return constraint
}
// MARK: Content Hugging and Compression Resistance
func setContentHuggingLow() {
setContentHuggingHorizontalLow()
setContentHuggingVerticalLow()
}
func setContentHuggingHigh() {
setContentHuggingHorizontalHigh()
setContentHuggingVerticalHigh()
}
func setContentHuggingHorizontalLow() {
setContentHuggingPriority(.defaultLow, for: .horizontal)
}
func setContentHuggingHorizontalHigh() {
setContentHuggingPriority(.required, for: .horizontal)
}
func setContentHuggingVerticalLow() {
setContentHuggingPriority(.defaultLow, for: .vertical)
}
func setContentHuggingVerticalHigh() {
setContentHuggingPriority(.required, for: .vertical)
}
func setCompressionResistanceLow() {
setCompressionResistanceHorizontalLow()
setCompressionResistanceVerticalLow()
}
func setCompressionResistanceHigh() {
setCompressionResistanceHorizontalHigh()
setCompressionResistanceVerticalHigh()
}
func setCompressionResistanceHorizontalLow() {
setContentCompressionResistancePriority(.init(0), for: .horizontal)
}
func setCompressionResistanceHorizontalHigh() {
setContentCompressionResistancePriority(.required, for: .horizontal)
}
func setCompressionResistanceVerticalLow() {
setContentCompressionResistancePriority(.init(0), for: .vertical)
}
func setCompressionResistanceVerticalHigh() {
setContentCompressionResistancePriority(.required, for: .vertical)
}
func deactivateAllConstraints() {
for constraint in constraints {
constraint.isActive = false
}
}
}
extension NSLayoutConstraint.Relation {
var inverse: NSLayoutConstraint.Relation {
switch self {
case .lessThanOrEqual: return .greaterThanOrEqual
case .equal: return .equal
case .greaterThanOrEqual: return .lessThanOrEqual
@unknown default:
owsFailDebug("Unknown case")
return .equal
}
}
}