Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/Views/ManualLayoutView.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit

// ManualLayoutView uses a CATransformLayer by default.
// CATransformLayer does not render.
//
// If you need to use properties like backgroundColor, border,
// masksToBounds, shadow, etc. you should use this subclass instead.
//
// See: https://developer.apple.com/documentation/quartzcore/catransformlayer
open class ManualLayoutViewWithLayer: ManualLayoutView {
    override open class var layerClass: AnyClass {
        CALayer.self
    }
}

// MARK: -

open class ManualLayoutView: UIView, CVView {

    public typealias LayoutBlock = (UIView) -> Void

    public typealias TransformBlock = (UIView) -> Void

    private var layoutBlocks = [LayoutBlock]()
    private var transformBlocks = [TransformBlock]()

    public let name: String

    override open class var layerClass: AnyClass {
        CATransformLayer.self
    }

    public init(name: String) {
        self.name = name

        super.init(frame: .zero)

        translatesAutoresizingMaskIntoConstraints = false

#if TESTABLE_BUILD
        self.accessibilityLabel = name
#endif
    }

    @available(*, unavailable, message: "use other constructor instead.")
    public required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        AssertIsOnMainThread()
    }

    public var shouldDeactivateConstraints = true

    override public func updateConstraints() {
        super.updateConstraints()

        if shouldDeactivateConstraints {
            deactivateAllConstraints()
        }
    }

    // MARK: - Circles and Pills

    public static func circleView(name: String) -> ManualLayoutView {
        let result = ManualLayoutViewWithLayer(name: name)
        result.addPillBlock()
        return result
    }

    public static func pillView(name: String) -> ManualLayoutView {
        let result = ManualLayoutViewWithLayer(name: name)
        result.addPillBlock()
        return result
    }

    // MARK: - Sizing

    public var preferredSize: CGSize?

    override open func sizeThatFits(_ size: CGSize) -> CGSize {
        preferredSize ?? .zero
    }

    override public var intrinsicContentSize: CGSize {
        return sizeThatFits(CGSize(
            width: CGFloat.greatestFiniteMagnitude,
            height: CGFloat.greatestFiniteMagnitude,
        ))
    }

    // MARK: - Layout

    override public var bounds: CGRect {
        didSet {
            if oldValue.size != bounds.size {
                viewSizeDidChange()
            }
        }
    }

    override public var frame: CGRect {
        didSet {
            if oldValue.size != frame.size {
                viewSizeDidChange()
            }
        }
    }

    func viewSizeDidChange() {
        AssertIsOnMainThread()

        layoutSubviews()
    }

    override open func layoutSubviews() {
        layoutSubviews(skipLayoutBlocks: false)
    }

    public func layoutSubviews(skipLayoutBlocks: Bool = false) {
        AssertIsOnMainThread()

        super.layoutSubviews()

        if !skipLayoutBlocks {
            applyLayoutBlocks()
        }
    }

    public func applyLayoutBlocks() {
        AssertIsOnMainThread()

        for layoutBlock in layoutBlocks {
            layoutBlock(self)
        }
    }

    public func applyTransformBlocks() {
        AssertIsOnMainThread()

        for transformBlock in transformBlocks {
            transformBlock(self)
        }

        for subview in subviews {
            if let manualLayoutSubview = subview as? ManualLayoutView {
                manualLayoutSubview.applyTransformBlocks()
            }
        }

        transformBlocks.removeAll()
    }

    public static func setSubviewFrame(subview: UIView, frame: CGRect) {
        guard subview.frame != frame else {
            return
        }
        subview.frame = frame
    }

    // MARK: - Reset

    open func reset() {
        AssertIsOnMainThread()

        removeAllSubviews()
        layoutBlocks.removeAll()
        transformBlocks.removeAll()

        invalidateIntrinsicContentSize()
        setNeedsLayout()

        self.tapBlock = nil
        if let gestureRecognizers = self.gestureRecognizers {
            for gestureRecognizer in gestureRecognizers {
                removeGestureRecognizer(gestureRecognizer)
            }
        }
    }

    // MARK: - Convenience Methods

    public func addPillBlock() {
        addLayoutBlock { view in
            view.layer.cornerRadius = view.bounds.size.smallerAxis * 0.5
        }
    }

    public func addSubview(
        _ subview: UIView,
        withLayoutBlock layoutBlock: @escaping LayoutBlock,
    ) {
        owsAssertDebug(subview.superview == nil)

        subview.translatesAutoresizingMaskIntoConstraints = false

        addSubview(subview)

        addLayoutBlock(layoutBlock)
    }

    public func addLayoutBlock(_ layoutBlock: @escaping LayoutBlock) {
        layoutBlocks.append(layoutBlock)
    }

    public func addTransformBlock(_ transformBlock: @escaping TransformBlock) {
        transformBlocks.append(transformBlock)
    }

    public func invalidateTransformBlocks() {
        transformBlocks.removeAll()
    }

    public func centerSubviewWithLayoutBlock(
        _ subview: UIView,
        onSiblingView siblingView: UIView,
        size: CGSize,
    ) {
        owsAssertDebug(subview.superview != nil)
        owsAssertDebug(subview.superview == siblingView.superview)

        subview.translatesAutoresizingMaskIntoConstraints = false

        addLayoutBlock { _ in
            guard let superview = subview.superview else {
                owsFailDebug("Missing superview.")
                return
            }
            owsAssertDebug(superview == siblingView.superview)

            let siblingCenter = superview.convert(
                siblingView.center,
                from: siblingView.superview,
            )
            let subviewOrigin = siblingCenter - (size.asPoint * 0.5)
            let subviewFrame = CGRect(origin: subviewOrigin, size: size)
            Self.setSubviewFrame(subview: subview, frame: subviewFrame)
        }
    }

    public func addSubviewToCenterOnSuperview(_ subview: UIView, size: CGSize) {
        owsAssertDebug(subview.superview == nil)

        addSubview(subview)

        centerSubviewOnSuperview(subview, size: size)
    }

    public func centerSubviewOnSuperview(_ subview: UIView, size: CGSize) {
        owsAssertDebug(subview.superview != nil)

        subview.translatesAutoresizingMaskIntoConstraints = false

        addLayoutBlock { _ in
            guard let superview = subview.superview else {
                owsFailDebug("Missing superview.")
                return
            }

            let superviewBounds = superview.bounds
            let subviewOrigin = ((superviewBounds.size - size) * 0.5).asPoint
            let subviewFrame = CGRect(origin: subviewOrigin, size: size)
            Self.setSubviewFrame(subview: subview, frame: subviewFrame)
        }
    }

    public func addSubviewToCenterOnSuperviewWithDesiredSize(_ subview: UIView) {
        owsAssertDebug(subview.superview == nil)

        addSubview(subview)

        centerSubviewOnSuperviewWithDesiredSize(subview)
    }

    public func centerSubviewOnSuperviewWithDesiredSize(_ subview: UIView) {
        owsAssertDebug(subview.superview != nil)

        subview.translatesAutoresizingMaskIntoConstraints = false

        addLayoutBlock { _ in
            guard let superview = subview.superview else {
                owsFailDebug("Missing superview.")
                return
            }

            let size = subview.sizeThatFitsMaxSize
            let superviewBounds = superview.bounds
            let subviewOrigin = ((superviewBounds.size - size) * 0.5).asPoint
            let subviewFrame = CGRect(origin: subviewOrigin, size: size)
            Self.setSubviewFrame(subview: subview, frame: subviewFrame)
        }
    }

    public func addSubviewToFillSuperviewEdges(_ subview: UIView) {
        owsAssertDebug(subview.superview == nil)

        addSubview(subview)

        layoutSubviewToFillSuperviewEdges(subview)
    }

    public func layoutSubviewToFillSuperviewEdges(_ subview: UIView) {
        layoutSubviewToFillSuperview(subview, honorLayoutsMargins: false)
    }

    public func addSubviewToFillSuperviewMargins(_ subview: UIView) {
        owsAssertDebug(subview.superview == nil)

        addSubview(subview)

        layoutSubviewToFillSuperviewMargins(subview)
    }

    public func layoutSubviewToFillSuperviewMargins(_ subview: UIView) {
        layoutSubviewToFillSuperview(subview, honorLayoutsMargins: true)
    }

    public func layoutSubviewToFillSuperview(
        _ subview: UIView,
        honorLayoutsMargins: Bool,
    ) {
        owsAssertDebug(subview.superview != nil)

        subview.translatesAutoresizingMaskIntoConstraints = false

        addLayoutBlock { _ in
            guard let superview = subview.superview else {
                owsFailDebug("Missing superview.")
                return
            }

            var subviewFrame = superview.bounds
            if honorLayoutsMargins {
                subviewFrame = subviewFrame.inset(by: superview.layoutMargins)
            }
            Self.setSubviewFrame(subview: subview, frame: subviewFrame)
        }
    }

    // MARK: - Gestures

    public typealias TapBlock = () -> Void
    private var tapBlock: TapBlock?

    public func addTapGesture(_ tapBlock: @escaping TapBlock) {
        self.tapBlock = tapBlock
        isUserInteractionEnabled = true
        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap)))
    }

    @objc
    private func didTap() {
        guard let tapBlock else {
            owsFailDebug("Missing tapBlock.")
            return
        }
        tapBlock()
    }
}

// MARK: -

public extension ManualLayoutView {

    static func wrapSubviewUsingIOSAutoLayout(
        _ subview: UIView,
        isWrapperRendering: Bool = false,
        wrapperName: String = "iOS auto layout wrapper",
    ) -> ManualLayoutView {
        let wrapper: ManualLayoutView
        if isWrapperRendering {
            wrapper = ManualLayoutViewWithLayer(name: wrapperName)
        } else {
            wrapper = ManualLayoutView(name: wrapperName)
        }
        wrapper.addSubviewToFillSuperviewEdges(subview)

        // blurView will be arranged by manual layout, but if we don't
        // constrain its width and height, its internal constraints will
        // be ambiguous.
        let widthConstraint = subview.autoSetDimension(.width, toSize: 0)
        let heightConstraint = subview.autoSetDimension(.height, toSize: 0)
        wrapper.addLayoutBlock { _ in
            widthConstraint.constant = subview.width
            heightConstraint.constant = subview.height
        }

        return wrapper
    }
}