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

import Combine
import SignalServiceKit

/// An interactive sheet view controller with stack view content. Automatically
/// resizes the sheet and enables/disables scrolling based on content size.
///
/// To use, set `contentStackView`'s `spacing` and `alignment`, and add your
/// content as arranged subviews. Optionally override `stackViewInsets` and/or
/// `minimumBottomInsetIncludingSafeArea`.
open class StackSheetViewController: InteractiveSheetViewController {
    override public var interactiveScrollViews: [UIScrollView] { [contentScrollView] }

    override open var sheetBackgroundColor: UIColor {
        UIColor.Signal.groupedBackground
    }

    override open var placeOnGlassIfAvailable: Bool { true }

    private lazy var preferredHeight: CGFloat = self.maximumAllowedHeight()
    override open func maximumPreferredHeight() -> CGFloat {
        min(self.preferredHeight, self.maximumAllowedHeight())
    }

    private var sizeChangeSubscription: AnyCancellable?

    /// Margins for the content in the stack view. The safe area insets for the
    /// bottom will be added to the value specified here. To set a minimum
    /// bottom inset, see ``minimumBottomInsetIncludingSafeArea``.
    ///
    /// Default value is 24 on all sides.
    open var stackViewInsets: UIEdgeInsets {
        .init(margin: 24)
    }

    /// The minimum inset for the bottom of the stack view, including the safe area.
    ///
    /// For example, if `stackViewInsets.bottom` is set to 20 and
    /// `minimumBottomInsetIncludingSafeArea` is set to 32, a device with a
    /// 40-pt bottom safe area inset will have a total bottom margin of
    /// 40+20 = 60, which is over the minimum. A device with no bottom safe area
    /// inset will use the minimum-specified bottom inset of 32.
    ///
    /// Default value is 0.
    open var minimumBottomInsetIncludingSafeArea: CGFloat { 0 }

    private let contentScrollView = UIScrollView()

    /// The stack view to add your main content to.
    /// Recommended to set a custom `spacing` and `alignment`.
    public lazy var stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.isLayoutMarginsRelativeArrangement = true
        return stackView
    }()

    override open func viewDidLoad() {
        super.viewDidLoad()

        allowsExpansion = false
        animationsShouldBeInterruptible = true
        contentView.addSubview(contentScrollView)
        contentScrollView.autoPinEdgesToSuperviewEdges()

        contentScrollView.addSubview(stackView)
        stackView.autoPinEdgesToSuperviewEdges()
        stackView.autoPinWidth(toWidthOf: contentView)

        sizeChangeSubscription = stackView
            .publisher(for: \.bounds)
            .removeDuplicates()
            .sink { [weak self] bounds in
                guard let self else { return }
                let desiredHeight = bounds.height + Constants.handleHeight
                self.preferredHeight = desiredHeight
                self.minimizedHeight = desiredHeight
                self.contentScrollView.isScrollEnabled = self.maxHeight < desiredHeight
            }
    }

    override open func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()

        let desiredInsets = self.stackViewInsets

        // Dragging the sheet up changes the stack view's safe area,
        // so add it in manually instead of inheriting it.
        let bottomMargin = max(
            self.view.safeAreaInsets.bottom + desiredInsets.bottom,
            minimumBottomInsetIncludingSafeArea,
        )

        contentScrollView.contentInsetAdjustmentBehavior = .never
        stackView.preservesSuperviewLayoutMargins = false
        stackView.insetsLayoutMarginsFromSafeArea = false
        stackView.layoutMargins = .init(
            top: desiredInsets.top,
            leading: desiredInsets.leading,
            bottom: bottomMargin,
            trailing: desiredInsets.trailing,
        )
    }
}