jsdw_ios/QuickLocation/UIKit/Pop/VersionUpgradePopVC.swift

365 lines
13 KiB
Swift

//
// 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
}
}