Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/UIKitExtensions/UIView+AutoLayout.swift
1 views
//
// 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
        }
    }
}