/* Copyright 2019-2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ import UIKit import Reusable protocol SlidingModalContainerViewDelegate: AnyObject { func slidingModalContainerViewDidTapBackground(_ view: SlidingModalContainerView) } /// `SlidingModalContainerView` is a custom UIView used as a `UIViewControllerContextTransitioning.containerView` subview to embed a `SlidingModalPresentable` during presentation. class SlidingModalContainerView: UIView, Themable, NibLoadable { // MARK: - Constants private enum Constants { static let cornerRadius: CGFloat = 12.0 static let dimmingColorAlpha: CGFloat = 0.7 } private enum Sizing { static let view = SlidingModalContainerView.loadFromNib() static var widthConstraint: NSLayoutConstraint? static var heightConstraint: NSLayoutConstraint? } // MARK: - Properties private weak var blurView: UIVisualEffectView? var blurBackground: Bool = false { didSet { if blurBackground { let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) blurView.frame = self.dimmingView.bounds blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.dimmingView.addSubview(blurView) self.blurView = blurView self.dimmingView.backgroundColor = .clear } else { self.blurView?.removeFromSuperview() self.dimmingView.backgroundColor = UIColor.black.withAlphaComponent(Constants.dimmingColorAlpha) } } } var centerInScreen: Bool = false // MARK: Outlets @IBOutlet private weak var dimmingView: UIView! @IBOutlet private weak var contentView: UIView! @IBOutlet private weak var contentViewHeightConstraint: NSLayoutConstraint! @IBOutlet private weak var contentViewBottomConstraint: NSLayoutConstraint! // MARK: Private private var dismissContentViewBottomConstant: CGFloat { let bottomSafeAreaHeight: CGFloat bottomSafeAreaHeight = self.contentView.safeAreaInsets.bottom return -(self.contentViewHeightConstraint.constant + bottomSafeAreaHeight) } // used to avoid changing constraint during animations private var lastBounds: CGRect? // MARK: Public var contentViewFrame: CGRect { return self.contentView.frame } weak var delegate: SlidingModalContainerViewDelegate? // MARK: - Setup static func instantiate() -> SlidingModalContainerView { return self.loadFromNib() } // MARK: - Life cycle override func awakeFromNib() { super.awakeFromNib() self.contentView.layer.masksToBounds = true self.dimmingView.backgroundColor = UIColor.black.withAlphaComponent(Constants.dimmingColorAlpha) self.setupBackgroundTapGestureRecognizer() self.update(theme: ThemeService.shared().theme) } override func layoutSubviews() { super.layoutSubviews() self.contentView.layer.cornerRadius = Constants.cornerRadius guard lastBounds != nil else { lastBounds = bounds return } if UIDevice.current.userInterfaceIdiom == .pad && lastBounds != bounds { lastBounds = bounds self.contentViewBottomConstraint.constant = (UIScreen.main.bounds.height + self.dismissContentViewBottomConstant) / 2 } } // MARK: - Public func preparePresentAnimation() { if UIDevice.current.userInterfaceIdiom == .pad { self.contentViewBottomConstraint.constant = (UIScreen.main.bounds.height + self.dismissContentViewBottomConstant) / 2 } else { if centerInScreen { contentViewBottomConstraint.constant = (bounds.height - contentViewHeightConstraint.constant)/2 } else { contentViewBottomConstraint.constant = 0 } } } func prepareDismissAnimation() { self.contentViewBottomConstraint.constant = self.dismissContentViewBottomConstant } func update(theme: Theme) { self.contentView.backgroundColor = theme.headerBackgroundColor } func updateContentViewMaxHeight(_ maxHeight: CGFloat) { self.contentViewHeightConstraint.constant = maxHeight } func updateContentViewLayout() { self.layoutIfNeeded() } func setContentView(_ contentView: UIView) { for subView in self.contentView.subviews { subView.removeFromSuperview() } self.contentView.vc_addSubViewMatchingParent(contentView) } func updateDimmingViewAlpha(_ alpha: CGFloat) { self.dimmingView.alpha = alpha } func contentViewWidthFittingSize(_ size: CGSize) -> CGFloat { let sizingView = SlidingModalContainerView.Sizing.view if let widthConstraint = SlidingModalContainerView.Sizing.widthConstraint { widthConstraint.constant = size.width } else { let widthConstraint = sizingView.widthAnchor.constraint(equalToConstant: size.width) widthConstraint.isActive = true SlidingModalContainerView.Sizing.widthConstraint = widthConstraint } if let heightConstraint = SlidingModalContainerView.Sizing.heightConstraint { heightConstraint.constant = size.height } else { let heightConstraint = sizingView.heightAnchor.constraint(equalToConstant: size.width) heightConstraint.isActive = true SlidingModalContainerView.Sizing.heightConstraint = heightConstraint } sizingView.setNeedsLayout() sizingView.layoutIfNeeded() return sizingView.contentViewFrame.width } // MARK: - Private private func setupBackgroundTapGestureRecognizer() { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundTap(_:))) self.dimmingView.addGestureRecognizer(tapGestureRecognizer) } @objc private func handleBackgroundTap(_ gestureRecognizer: UITapGestureRecognizer) { self.delegate?.slidingModalContainerViewDidTapBackground(self) } }