// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import SessionUtilitiesKit open class Modal: UIViewController, UIGestureRecognizerDelegate { private static let cornerRadius: CGFloat = 11 public enum DismissType: Equatable, Hashable { case single case recursive } private let dismissType: DismissType private let afterClosed: (() -> ())? // MARK: - Components private lazy var dimmingView: UIView = { let result = UIVisualEffectView() ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in result?.effect = UIBlurEffect( style: (theme.interfaceStyle == .light ? UIBlurEffect.Style.systemUltraThinMaterialLight : UIBlurEffect.Style.systemUltraThinMaterial ) ) } return result }() lazy var containerView: UIView = { let result: UIView = UIView() result.clipsToBounds = false result.themeBackgroundColor = .alert_background result.themeShadowColor = .black result.layer.cornerRadius = Modal.cornerRadius result.layer.shadowRadius = 10 result.layer.shadowOpacity = 0.4 return result }() public lazy var contentView: UIView = { let result: UIView = UIView() result.clipsToBounds = true result.layer.cornerRadius = Modal.cornerRadius return result }() public lazy var cancelButton: UIButton = { let result: UIButton = Modal.createButton(title: "cancel".localized(), titleColor: .textPrimary) result.addTarget(self, action: #selector(cancel), for: .touchUpInside) return result }() // MARK: - Lifecycle public init( targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil ) { self.dismissType = dismissType self.afterClosed = afterClosed super.init(nibName: nil, bundle: nil) // Ensure the modal doesn't crash on iPad when being presented Modal.setupForIPadIfNeeded(self, targetView: (targetView ?? self.view)) } required public init?(coder: NSCoder) { fatalError("Use init(targetView:afterClosed:) instead") } public override func viewDidLoad() { super.viewDidLoad() navigationItem.backButtonTitle = "" view.themeBackgroundColor = .clear ThemeManager.applyNavigationStylingIfNeeded(to: self) setNeedsStatusBarAppearanceUpdate() view.addSubview(dimmingView) view.addSubview(containerView) containerView.addSubview(contentView) dimmingView.pin(to: view) contentView.pin(to: containerView) if UIDevice.current.isIPad { containerView.set(.width, to: Values.iPadModalWidth) containerView.center(in: view) } else { containerView.leadingAnchor .constraint(equalTo: view.leadingAnchor, constant: Values.veryLargeSpacing) .isActive = true view.trailingAnchor .constraint(equalTo: containerView.trailingAnchor, constant: Values.veryLargeSpacing) .isActive = true containerView.center(.vertical, in: view) } // Gestures let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close)) swipeGestureRecognizer.direction = .down dimmingView.addGestureRecognizer(swipeGestureRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(close)) tapGestureRecognizer.delegate = self dimmingView.addGestureRecognizer(tapGestureRecognizer) populateContentView() } /// To be overridden by subclasses. open func populateContentView() { preconditionFailure("populateContentView() is abstract and must be overridden.") } public static func createButton(title: String, titleColor: ThemeValue) -> UIButton { let result: UIButton = UIButton() result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.setTitle(title, for: .normal) result.setThemeTitleColor(titleColor, for: .normal) result.setThemeBackgroundColor(.alert_buttonBackground, for: .normal) result.setThemeBackgroundColor(.highlighted(.alert_buttonBackground), for: .highlighted) result.set(.height, to: Values.alertButtonHeight) return result } // MARK: - Interaction @objc public func cancel() { close() } @objc public final func close() { // Recursively dismiss all modals (ie. find the first modal presented by a non-modal // and get that to dismiss it's presented view controller) var targetViewController: UIViewController? = self switch dismissType { case .single: break case .recursive: while targetViewController?.presentingViewController is Modal { targetViewController = targetViewController?.presentingViewController } } targetViewController?.presentingViewController?.dismiss(animated: true) { [weak self] in self?.afterClosed?() } } // MARK: - UIGestureRecognizerDelegate public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { let location: CGPoint = touch.location(in: contentView) return !contentView.point(inside: location, with: nil) } } // MARK: - Convenience public extension Modal { static func setupForIPadIfNeeded(_ viewController: UIViewController, targetView: UIView) { if UIDevice.current.isIPad { viewController.popoverPresentationController?.permittedArrowDirections = [] viewController.popoverPresentationController?.sourceView = targetView viewController.popoverPresentationController?.sourceRect = targetView.bounds } } }