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