// // VersionUpgradePopVC.swift // QuickLocation // import UIKit final class VersionUpgradePopVC: UIViewController { var completion: (() -> Void)? var dismissHandler: ((VersionUpgradePopVC, @escaping () -> Void) -> Void)? private let upgrade: VersionUpgradeModel private var isOpeningURL = false private var hasFinished = false private var contentTextViewHeightConstraint: NSLayoutConstraint? init(upgrade: VersionUpgradeModel) { self.upgrade = upgrade super.init(nibName: nil, bundle: nil) modalPresentationStyle = .overFullScreen modalTransitionStyle = .crossDissolve } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupUI() configureContent() } func showAnimated() { view.layoutIfNeeded() dimmingView.alpha = 0 popupContainerView.alpha = 0 popupContainerView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94) UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut]) { self.dimmingView.alpha = 1 self.popupContainerView.alpha = 1 self.popupContainerView.transform = .identity } } private func setupUI() { view.backgroundColor = .clear view.addSubview(dimmingView) dimmingView.translatesAutoresizingMaskIntoConstraints = false if !upgrade.force { dimmingView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelUpgrade))) } view.addSubview(popupContainerView) popupContainerView.addSubview(cardView) popupContainerView.addSubview(headerImageView) popupContainerView.addSubview(cancelButton) cardView.addSubview(contentTextView) cardView.addSubview(upgradeButton) [popupContainerView, cardView, headerImageView, cancelButton, contentTextView, upgradeButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } let contentTextViewHeightConstraint = contentTextView.heightAnchor.constraint(equalToConstant: 96) self.contentTextViewHeightConstraint = contentTextViewHeightConstraint NSLayoutConstraint.activate([ dimmingView.topAnchor.constraint(equalTo: view.topAnchor), dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), popupContainerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), popupContainerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), popupContainerView.widthAnchor.constraint(equalToConstant: 325), popupContainerView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 20), popupContainerView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -20), headerImageView.topAnchor.constraint(equalTo: popupContainerView.topAnchor), headerImageView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor), headerImageView.widthAnchor.constraint(equalTo: cardView.widthAnchor, constant: 10), headerImageView.heightAnchor.constraint(equalTo: headerImageView.widthAnchor, multiplier: 132 / 325), cardView.topAnchor.constraint(equalTo: popupContainerView.topAnchor, constant: 88), cardView.centerXAnchor.constraint(equalTo: popupContainerView.centerXAnchor), cardView.widthAnchor.constraint(equalToConstant: 315), contentTextView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 40), contentTextView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 20), contentTextView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -20), contentTextViewHeightConstraint, upgradeButton.topAnchor.constraint(equalTo: contentTextView.bottomAnchor, constant: 28), upgradeButton.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor), upgradeButton.trailingAnchor.constraint(equalTo: contentTextView.trailingAnchor), upgradeButton.heightAnchor.constraint(equalToConstant: 50), upgradeButton.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -20), cancelButton.topAnchor.constraint(equalTo: cardView.bottomAnchor, constant: 14), cancelButton.centerXAnchor.constraint(equalTo: popupContainerView.centerXAnchor), cancelButton.heightAnchor.constraint(equalToConstant: 28), cancelButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 80), cancelButton.bottomAnchor.constraint(equalTo: popupContainerView.bottomAnchor) ]) } private func configureContent() { cancelButton.isHidden = upgrade.force let title = upgrade.title + upgrade.version let description = upgrade.description let content = NSMutableAttributedString() if !title.isEmpty { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = 3 content.append(NSAttributedString( string: title, attributes: [ .font: UIFont.systemFont(ofSize: 15, weight: .medium), .foregroundColor: UIColor(hexStr: "#273246"), .paragraphStyle: paragraphStyle ] )) } if !description.isEmpty { if !content.string.isEmpty { content.append(NSAttributedString(string: "\n")) } let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = 5 content.append(NSAttributedString( string: description, attributes: [ .font: UIFont.systemFont(ofSize: 15, weight: .regular), .foregroundColor: UIColor(hexStr: "#273246"), .paragraphStyle: paragraphStyle ] )) } contentTextView.attributedText = content contentTextView.setContentOffset(.zero, animated: false) let fittingSize = contentTextView.sizeThatFits( CGSize(width: 275, height: CGFloat.greatestFiniteMagnitude) ) contentTextViewHeightConstraint?.constant = max(96, ceil(fittingSize.height)) } @objc private func openUpgradeURL() { guard !isOpeningURL else { return } let urlString = upgrade.url.trimmingCharacters(in: .whitespacesAndNewlines) guard let url = URL(string: urlString), let scheme = url.scheme, !scheme.isEmpty else { DLToast.show(text: "下载地址不可用") return } isOpeningURL = true UIApplication.shared.open(url, options: [:]) { [weak self] opened in DispatchQueue.main.async { guard let self else { return } self.isOpeningURL = false guard opened else { DLToast.show(text: "下载地址不可用") return } if !self.upgrade.force { self.finishUpgrade() } } } } @objc private func cancelUpgrade() { guard !upgrade.force else { return } finishUpgrade() } private func finishUpgrade() { guard !hasFinished else { return } hasFinished = true let finish = { [weak self] in guard let self else { return } let completion = self.completion self.completion = nil completion?() } guard let dismissHandler else { dismiss(animated: true, completion: finish) return } dismissHandler(self, finish) } private lazy var dimmingView: UIView = { let view = UIView() view.backgroundColor = UIColor.black.withAlphaComponent(0.68) return view }() private let popupContainerView = UIView() private lazy var cardView: UIView = { let cardView = UIView() cardView.backgroundColor = .white cardView.layer.cornerRadius = 36 cardView.layer.masksToBounds = true return cardView }() private lazy var headerImageView: UIImageView = { let imageView = UIImageView(image: UIImage(named: "VersionUpgrade/header")) imageView.contentMode = .scaleAspectFit return imageView }() private lazy var contentTextView: UITextView = { let textView = UITextView() textView.backgroundColor = UIColor(hexStr: "#F0FAFF") textView.layer.cornerRadius = 26 textView.layer.masksToBounds = true textView.isEditable = false textView.isSelectable = false textView.isScrollEnabled = false textView.showsVerticalScrollIndicator = false textView.textContainerInset = UIEdgeInsets(top: 15, left: 35, bottom: 15, right: 35) textView.textContainer.lineFragmentPadding = 0 return textView }() private lazy var upgradeButton: VersionUpgradeGradientButton = { let button = VersionUpgradeGradientButton(type: .custom) button.setTitle("立即体验", for: .normal) button.setTitleColor(.white, for: .normal) button.titleLabel?.font = .systemFont(ofSize: 18, weight: .medium) button.addTarget(self, action: #selector(openUpgradeURL), for: .touchUpInside) return button }() private lazy var cancelButton: UIButton = { let button = UIButton(type: .custom) button.setTitle("取消", for: .normal) button.setTitleColor(.white, for: .normal) button.titleLabel?.font = .systemFont(ofSize: 17, weight: .regular) button.addTarget(self, action: #selector(cancelUpgrade), for: .touchUpInside) return button }() } private final class VersionUpgradeGradientButton: UIButton { private let gradientLayer = CAGradientLayer() override init(frame: CGRect) { super.init(frame: frame) gradientLayer.colors = [ UIColor(hexStr: "#08B5FF").cgColor, UIColor(hexStr: "#48C7FF").cgColor ] gradientLayer.startPoint = CGPoint(x: 0, y: 0.5) gradientLayer.endPoint = CGPoint(x: 1, y: 0.5) layer.insertSublayer(gradientLayer, at: 0) layer.masksToBounds = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() gradientLayer.frame = bounds layer.cornerRadius = 16 } } extension UIViewController { func showVersionUpgradePop( upgrade: VersionUpgradeModel, completion: (() -> Void)? = nil ) { VersionUpgradeWindowPresenter.show( upgrade: upgrade, preferredScene: viewIfLoaded?.window?.windowScene, completion: completion ) } } private enum VersionUpgradeWindowPresenter { private static var hostWindow: UIWindow? private static weak var previousKeyWindow: UIWindow? static func show( upgrade: VersionUpgradeModel, preferredScene: UIWindowScene?, completion: (() -> Void)? ) { let showBlock = { guard hostWindow == nil, let windowScene = preferredScene ?? activeWindowScene() else { return } previousKeyWindow = windowScene.windows.first { $0.isKeyWindow } let viewController = VersionUpgradePopVC(upgrade: upgrade) viewController.completion = completion viewController.dismissHandler = { viewController, finish in dismiss(viewController, finish: finish) } let window = UIWindow(windowScene: windowScene) window.frame = windowScene.coordinateSpace.bounds window.windowLevel = .alert + 1 window.backgroundColor = .clear window.rootViewController = viewController hostWindow = window window.makeKeyAndVisible() viewController.showAnimated() } if Thread.isMainThread { showBlock() } else { DispatchQueue.main.async(execute: showBlock) } } private static func dismiss( _ viewController: VersionUpgradePopVC, finish: @escaping () -> Void ) { guard hostWindow?.rootViewController === viewController else { finish() return } UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn]) { viewController.view.alpha = 0 } completion: { _ in hostWindow?.isHidden = true hostWindow?.rootViewController = nil hostWindow = nil previousKeyWindow?.makeKey() previousKeyWindow = nil finish() } } private static func activeWindowScene() -> UIWindowScene? { let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } return scenes.first { $0.activationState == .foregroundActive } ?? scenes.first { $0.activationState == .foregroundInactive } ?? scenes.first } }