373 lines
12 KiB
Swift
373 lines
12 KiB
Swift
//
|
||
// EGNewBaseAlert.swift
|
||
// Crush
|
||
//
|
||
// Created by Leon on 2025/8/3.
|
||
//
|
||
|
||
import UIKit
|
||
import SnapKit
|
||
|
||
// MARK: - EGNewAlertAction
|
||
|
||
enum EGNewAlertActionStyle: Int {
|
||
case `default`
|
||
case cancel
|
||
case confirm
|
||
case disabled
|
||
case inputSave
|
||
case destructive
|
||
}
|
||
|
||
class EGNewAlertAction : Equatable {
|
||
var title: String
|
||
var attributedTitle: NSAttributedString?
|
||
var actionStyle: EGNewAlertActionStyle
|
||
var autoDismiss: Bool
|
||
var actionBlock: (() -> Void)?
|
||
|
||
init(title: String, actionStyle: EGNewAlertActionStyle = .default, autoDismiss: Bool = true, block: (() -> Void)? = nil) {
|
||
self.title = title
|
||
self.attributedTitle = nil
|
||
self.actionStyle = actionStyle
|
||
self.autoDismiss = autoDismiss
|
||
self.actionBlock = block
|
||
}
|
||
|
||
// 新增:使用 NSAttributedString 的初始化方法
|
||
init(attributedTitle: NSAttributedString, actionStyle: EGNewAlertActionStyle = .default, autoDismiss: Bool = true, block: (() -> Void)? = nil) {
|
||
self.title = "" // 当使用富文本时,title 为空
|
||
self.attributedTitle = attributedTitle
|
||
self.actionStyle = actionStyle
|
||
self.autoDismiss = autoDismiss
|
||
self.actionBlock = block
|
||
}
|
||
|
||
static func action(title: String, block: (() -> Void)? = nil) -> EGNewAlertAction {
|
||
return EGNewAlertAction(title: title, block: block)
|
||
}
|
||
|
||
static func action(title: String, actionStyle: EGNewAlertActionStyle, block: (() -> Void)? = nil) -> EGNewAlertAction {
|
||
return EGNewAlertAction(title: title, actionStyle: actionStyle, block: block)
|
||
}
|
||
|
||
static func action(title: String, actionStyle: EGNewAlertActionStyle, autoDismiss: Bool, block: (() -> Void)? = nil) -> EGNewAlertAction {
|
||
return EGNewAlertAction(title: title, actionStyle: actionStyle, autoDismiss: autoDismiss, block: block)
|
||
}
|
||
|
||
// 新增:使用 NSAttributedString 的静态方法
|
||
static func action(attributedTitle: NSAttributedString, block: (() -> Void)? = nil) -> EGNewAlertAction {
|
||
return EGNewAlertAction(attributedTitle: attributedTitle, block: block)
|
||
}
|
||
|
||
static func action(attributedTitle: NSAttributedString, actionStyle: EGNewAlertActionStyle, block: (() -> Void)? = nil) -> EGNewAlertAction {
|
||
return EGNewAlertAction(attributedTitle: attributedTitle, actionStyle: actionStyle, block: block)
|
||
}
|
||
|
||
static func action(attributedTitle: NSAttributedString, actionStyle: EGNewAlertActionStyle, autoDismiss: Bool, block: (() -> Void)? = nil) -> EGNewAlertAction {
|
||
return EGNewAlertAction(attributedTitle: attributedTitle, actionStyle: actionStyle, autoDismiss: autoDismiss, block: block)
|
||
}
|
||
|
||
// MARK: - Equatable
|
||
static func == (lhs: EGNewAlertAction, rhs: EGNewAlertAction) -> Bool {
|
||
return lhs === rhs // 🔥 Using identity comparison; adjust if unique identifier exists
|
||
}
|
||
}
|
||
|
||
// MARK: - EGNewBaseAlert
|
||
|
||
enum EGNewAlertPriority: Int {
|
||
case `default` = 0
|
||
case update = 10
|
||
case forceUpdate = 100
|
||
}
|
||
|
||
class EGNewBaseAlert: UIView {
|
||
// MARK: - Properties
|
||
private(set) var backgroundView: UIView!
|
||
private(set) var containerView: UIView!
|
||
private(set) var textContentView: UIView!
|
||
private(set) var buttonContainer: UIView!
|
||
private(set) var buttons: [StyleButton] = []
|
||
private(set) var actions: [EGNewAlertAction] = []
|
||
var priority: EGNewAlertPriority = .default
|
||
private var maxActionCount: Int = 2
|
||
|
||
var containerWidth: CGFloat {
|
||
return UIScreen.main.bounds.width - containerMarginLR() * 2
|
||
}
|
||
|
||
// MARK: - Initialization
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
baseDataInit()
|
||
baseUIInit()
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
super.init(coder: coder)
|
||
baseDataInit()
|
||
baseUIInit()
|
||
}
|
||
|
||
private func baseDataInit() {
|
||
maxActionCount = 2
|
||
buttons = []
|
||
actions = []
|
||
}
|
||
|
||
private func baseUIInit() {
|
||
backgroundView = UIView().then {
|
||
addSubview($0)
|
||
$0.backgroundColor = .c.cbs
|
||
$0.alpha = 0
|
||
$0.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
containerView = UIView().then {
|
||
addSubview($0)
|
||
$0.backgroundColor = .c.csbn // 🔥 Assuming EPS_Color_Surface_base_normal is a light color
|
||
$0.layer.cornerRadius = containerCornerRadius()
|
||
$0.clipsToBounds = true
|
||
$0.alpha = 0
|
||
let width = UIScreen.main.bounds.width * 0.8
|
||
$0.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.width.equalTo(width)
|
||
}
|
||
}
|
||
|
||
textContentView = UIView().then {
|
||
containerView.addSubview($0)
|
||
$0.backgroundColor = .clear
|
||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||
$0.snp.makeConstraints { make in
|
||
make.leading.trailing.top.equalToSuperview()
|
||
make.height.greaterThanOrEqualTo(textContentMinHeight())
|
||
}
|
||
}
|
||
|
||
buttonContainer = UIView().then {
|
||
containerView.addSubview($0)
|
||
$0.backgroundColor = .clear
|
||
$0.snp.makeConstraints { make in
|
||
make.top.equalTo(textContentView.snp.bottom)
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Layout Methods (Overridable)
|
||
func containerMarginLR() -> CGFloat {
|
||
return 40
|
||
}
|
||
|
||
func textContentMaxHeight() -> CGFloat {
|
||
return UIScreen.main.bounds.height * 0.65
|
||
}
|
||
|
||
func textContentMinHeight() -> CGFloat {
|
||
return 100
|
||
}
|
||
|
||
func containerCornerRadius() -> CGFloat {
|
||
return 16
|
||
}
|
||
|
||
func containerAlphaDuration() -> CGFloat {
|
||
return 0.35
|
||
}
|
||
|
||
func backgroundAlphaValue() -> CGFloat {
|
||
return 0.65
|
||
}
|
||
|
||
func alertInitRatio() -> CGFloat {
|
||
return 0.6
|
||
}
|
||
|
||
// MARK: - Actions
|
||
func addAction(_ action: EGNewAlertAction) {
|
||
addAction(action, propertySetup: nil)
|
||
}
|
||
|
||
func addAction(_ action: EGNewAlertAction, propertySetup: ((UIButton) -> Void)?) {
|
||
guard actions.count < maxActionCount else { return }
|
||
|
||
let button = StyleButton(type: .custom)
|
||
button.addTarget(self, action: #selector(baseButtonPressed(_:)), for: .touchUpInside)
|
||
|
||
// 支持富文本和普通文本
|
||
if let attributedTitle = action.attributedTitle {
|
||
button.setAttributedTitle(attributedTitle, for: .normal)
|
||
} else {
|
||
button.setTitle(action.title, for: .normal)
|
||
}
|
||
|
||
button.tag = actions.count
|
||
button.titleLabel?.font = .t.tbsl// EPS_Txt_BodySemibold_l is a bold font
|
||
reloadButton(button, style: action.actionStyle)
|
||
|
||
if let setup = propertySetup {
|
||
setup(button)
|
||
}
|
||
|
||
actions.append(action)
|
||
buttons.append(button)
|
||
}
|
||
|
||
func reloadAction(_ action: EGNewAlertAction, title: String) {
|
||
guard let index = actions.firstIndex(of: action) else {
|
||
assertionFailure("Action not found")
|
||
return
|
||
}
|
||
let button = buttons[index]
|
||
button.setTitle(title, for: .normal)
|
||
reloadButton(button, style: action.actionStyle)
|
||
}
|
||
|
||
// 新增:支持富文本的 reloadAction 方法
|
||
func reloadAction(_ action: EGNewAlertAction, attributedTitle: NSAttributedString) {
|
||
guard let index = actions.firstIndex(of: action) else {
|
||
assertionFailure("Action not found")
|
||
return
|
||
}
|
||
let button = buttons[index]
|
||
button.setAttributedTitle(attributedTitle, for: .normal)
|
||
reloadButton(button, style: action.actionStyle)
|
||
}
|
||
|
||
private func reloadButton(_ button: StyleButton, style: EGNewAlertActionStyle) {
|
||
button.isEnabled = true
|
||
switch style {
|
||
case .default, .cancel:
|
||
button.tertiary(size: .large)
|
||
case .confirm,.inputSave:
|
||
button.primary(size: .large)
|
||
case .disabled:
|
||
button.tertiary(size: .large)
|
||
button.isEnabled = false
|
||
case .destructive:
|
||
button.defaultDestructive(size: .large)
|
||
}
|
||
}
|
||
|
||
// MARK: - Events
|
||
@objc private func baseButtonPressed(_ button: UIButton) {
|
||
endEditing(true)
|
||
let action = actions[button.tag]
|
||
if action.autoDismiss {
|
||
dismiss()
|
||
}
|
||
action.actionBlock?()
|
||
}
|
||
|
||
func alertDidShow() {
|
||
// Overridable by subclasses
|
||
}
|
||
|
||
// MARK: - Animation
|
||
func showWithAnimation() {
|
||
containerView.transform = CGAffineTransform(scaleX: alertInitRatio(), y: alertInitRatio())
|
||
UIView.animate(withDuration: containerAlphaDuration(), delay: 0, options: .curveEaseOut, animations: {
|
||
self.backgroundView.alpha = self.backgroundAlphaValue()
|
||
self.containerView.alpha = 1
|
||
self.containerView.transform = .identity
|
||
}) { _ in
|
||
self.alertDidShow()
|
||
}
|
||
}
|
||
|
||
func dismissWithAnimation() {
|
||
UIView.animate(withDuration: containerAlphaDuration(), delay: 0, options: .curveEaseIn, animations: {
|
||
self.containerView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
|
||
self.backgroundView.alpha = 0
|
||
self.containerView.alpha = 0
|
||
}) { _ in
|
||
self.removeFromSuperview()
|
||
}
|
||
}
|
||
|
||
// MARK: - Display
|
||
func show() {
|
||
// ⚠️ Before is windows's first
|
||
//guard let window = UIApplication.shared.windows.first else { return }
|
||
|
||
// 第二种方式:
|
||
// var window: UIWindow!
|
||
// for per in UIApplication.shared.windows.reversed(){
|
||
// if per.isHidden == false{
|
||
// window = per
|
||
// break
|
||
// }
|
||
// }
|
||
|
||
// 第3种方式: 会过滤一些
|
||
var window: UIWindow!
|
||
window = UIWindow.getTopDisplayWindow()!
|
||
|
||
if window == nil{
|
||
assert(false, "alert window not found")
|
||
}
|
||
|
||
window.endEditing(true)
|
||
|
||
// 隐藏已有的Alert(覆盖显示)
|
||
for subview in window.subviews {
|
||
if let oldAlert = subview as? EGNewBaseAlert {
|
||
if oldAlert.priority != .default && oldAlert.priority.rawValue >= priority.rawValue {
|
||
return
|
||
} else {
|
||
oldAlert.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|
||
window.addSubview(self)
|
||
self.frame = window.bounds
|
||
showWithAnimation()
|
||
|
||
Hud.hideIndicator()
|
||
}
|
||
|
||
func dismiss() {
|
||
dismissWithAnimation()
|
||
}
|
||
|
||
static func existingAlert() -> Bool {
|
||
return existingAlertObj() != nil
|
||
}
|
||
|
||
static func existingAlertObj() -> EGNewBaseAlert? {
|
||
//guard let window = UIApplication.shared.windows.first else { return nil }
|
||
guard let window = UIWindow.getTopDisplayWindow() else { return nil }
|
||
return window.subviews.first(where: { $0 is EGNewBaseAlert }) as? EGNewBaseAlert
|
||
}
|
||
|
||
static func hideAllAlert() {
|
||
guard let window = UIApplication.shared.windows.first else { return }
|
||
for subview in window.subviews {
|
||
if let alert = subview as? EGNewBaseAlert {
|
||
alert.isHidden = true
|
||
alert.removeFromSuperview()
|
||
}
|
||
}
|
||
}
|
||
|
||
func setupMaxActionCount(_ count: Int) {
|
||
maxActionCount = count
|
||
}
|
||
}
|
||
|
||
// MARK: - Then Protocol for Chaining
|
||
protocol Then {}
|
||
extension Then where Self: AnyObject {
|
||
func then(_ block: (Self) -> Void) -> Self {
|
||
block(self)
|
||
return self
|
||
}
|
||
}
|
||
extension UIView: Then {}
|