319 lines
12 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|