// // DLToast.swift // DLSDK // // Created by osell on 2023/8/18. // import UIKit // MARK: - DLToastConfig public class DLToastConfig { fileprivate static var tag = NSStringFromClass(DLToastView.self).hash public enum Style { case none case loading case progress } /// 背景图层颜色 var bgColor: UIColor = .clear /// 容器背景色 var containerBgColor: UIColor = .black.withAlphaComponent(0.7) /// 容器圆角 var containerRadius: CGFloat = 12.0 /// 文字颜色 var textColor: UIColor = .white /// 文字大小 var textFont: UIFont = .systemFont(ofSize: 16, weight: .medium) /// 图标大小 var iconSize: CGSize = CGSize(width: 34, height: 34) public init() {} } // MARK: - ToastImageView class ToastImageView: UIImageView { private var displayLink: CADisplayLink? private var rotationAngle: CGFloat = 0.0 func startAnimation() { displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:))) displayLink?.add(to: .current, forMode: .common) } @objc private func handleDisplayLink(_ displayLink: CADisplayLink) { rotationAngle += CGFloat(displayLink.duration) * 2.0 * .pi transform = CGAffineTransform(rotationAngle: rotationAngle) } func stopAnimation() { displayLink?.remove(from: .current, forMode: .common) displayLink?.invalidate() displayLink = nil } deinit { stopAnimation() } } // MARK: - DLToastView class DLToastView: UIView { /// 完成后回调 var completion: (() -> Void)? /// container private lazy var containerView: UIView = { let view = UIView() view.backgroundColor = config.containerBgColor view.setCornerRadius(config.containerRadius) view.isUserInteractionEnabled = true return view }() /// 图标 private lazy var iconImageView: ToastImageView = { let view = ToastImageView() return view }() /// text private lazy var textLabel: UILabel = { let label = UILabel() label.font = config.textFont label.textColor = config.textColor label.textAlignment = .center label.numberOfLines = 0 return label }() private weak var viewForPresent: UIView? private var config: DLToastConfig = DLToastConfig() private var style: DLToastConfig.Style = .none private var text: String? private var iconImage: UIImage? private var timer: DispatchSourceTimer? private override init(frame: CGRect) { super.init(frame: .zero) setupSubViews() } init(iconImage: UIImage?, text: String?, config: DLToastConfig, style: DLToastConfig.Style) { self.iconImage = iconImage self.text = text self.config = config self.style = style super.init(frame: .zero) setupSubViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 初始化视图布局 private func setupSubViews() { // 视图容器 addSubview(containerView) containerView.layoutChain .center() .left(48, relation: .greaterThanOrEqual) .right(48, relation: .greaterThanOrEqual) .top(kNaviHeight, relation: .greaterThanOrEqual) .bottom(kNaviHeight, relation: .greaterThanOrEqual) .width(100, relation: .greaterThanOrEqual) // 是否显示文字 let isShowText = text?.isEmpty == false var preView: UIView? // 图片 if let iconImage = iconImage { iconImageView.image = iconImage containerView.addSubview(iconImageView) iconImageView.layoutChain .centerX() .size(config.iconSize) .top(isShowText ? 30 : 33) .left(33, relation: isShowText ? .greaterThanOrEqual : .equal) .right(33, relation: isShowText ? .greaterThanOrEqual : .equal) preView = iconImageView } // 文字 if isShowText { textLabel.text = text containerView.addSubview(textLabel) textLabel.layoutChain .edgesHorzontal(16) .width(112, relation: .greaterThanOrEqual) if let preView = preView { textLabel.layoutChain .topToBottomOfView(preView, offset: 20) } else { textLabel.layoutChain .top(12) } preView = textLabel } if let preView = preView as? UIImageView { preView.layoutChain .bottom(33) } else if let preView = preView as? UILabel { preView.layoutChain .bottom(isShowText && iconImage != nil ? 30 : 12) } } deinit { NotificationCenter.default.removeObserver(self) if timer != nil { timer?.cancel() timer = nil } } } extension DLToastView { /// 显示Toast /// - Parameter view: toast父视图 func show(in view: UIView) { viewForPresent = view guard let viewForPresent = viewForPresent else { return } backgroundColor = .clear isUserInteractionEnabled = style == .loading viewForPresent.addSubview(self) self.layoutChain .edges() alpha = 0 sizeToFit() containerView.center = .init(x: viewForPresent.frame.midX, y: viewForPresent.frame.midY) containerView.transform = transform.scaledBy(x: 0.8, y: 0.8) UIView.animate(withDuration: 0.25) { self.alpha = 1 self.containerView.transform = CGAffineTransform.identity self.backgroundColor = self.config.bgColor } // loading if style == .loading { NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) // 执行动画 iconImageView.startAnimation() } // 根据文本内容自动消失 if style == .none { let text = text ?? "" var time = Double(text.count) * 0.03 + 1.25 time = time > 3.0 ? 3.0 : time self.dismiss(delay: time, self.completion) } } /// 隐藏 /// - Parameter completion: 完成回调 func dismiss(delay: TimeInterval = 0, _ completion: (() -> Void)? = nil) { // 有文字显示,自动消失 if timer != nil { timer?.cancel() timer = nil } let queue = DispatchQueue(label: "com.toast.timer", qos: .background) let timer = DispatchSource.makeTimerSource(queue: queue) timer.schedule(deadline: .now() + delay) timer.setEventHandler { [weak self] in guard let this = self else { return } DispatchQueue.main.async { UIView.animate(withDuration: 0.25) { this.alpha = 0 this.containerView.transform = this.containerView.transform.scaledBy(x: 0.2, y: 0.2) } completion: { _ in if this.style == .loading { this.iconImageView.stopAnimation() } this.removeFromSuperview() completion?() } } } timer.resume() self.timer = timer } @objc func applicationWillResignActive() { if style == .loading { iconImageView.stopAnimation() } } @objc func applicationDidBecomeActive() { if style == .loading { iconImageView.startAnimation() } } } // MARK: - 通用Toast public class DLToast { /// 显示loading /// - Parameters: /// - text: 文字 /// - view: 在哪个视图上显示,默认在window public static func showLoading(text: String? = nil, in view: UIView? = nil) { show(text: text, icon: UIImage(named: "loading"), style: .loading, in: view) } /// 显示成功 /// - Parameters: /// - text: 文字 /// - view: 在哪个视图上显示,默认在window /// - completion: 完成后回调 public static func showSuccess(text: String? = nil, in view: UIView? = nil, completion: (() -> Void)? = nil) { show(text: text, icon: UIImage(named: "success"), in: view, completion: completion) } /// 显示错误 /// - Parameters: /// - text: 文字 /// - view: 在哪个视图上显示,默认在window /// - completion: 完成后回调 public static func showError(text: String? = nil, in view: UIView? = nil, completion: (() -> Void)? = nil) { show(text: text, icon: UIImage(named: "fail"), in: view, completion: completion) } /// 显示警告信息 /// - Parameters: /// - text: 文字 /// - view: 在哪个视图上显示,默认在window /// - completion: 完成后回调 public static func showInfo(text: String? = nil, in view: UIView? = nil, completion: (() -> Void)? = nil) { show(text: text, icon: UIImage(named: "warning"), in: view, completion: completion) } /// 显示toast /// - Parameters: /// - text: 文字 /// - icon: 图标 /// - config: 配置信息 /// - style: 样式 /// - view: 在哪个视图上显示,默认在window /// - completion: 完成后回调 public static func show(text: String? = nil, icon: UIImage? = nil, config: DLToastConfig = DLToastConfig(), style: DLToastConfig.Style = .none, in view: UIView? = nil, completion: (() -> Void)? = nil) { DispatchQueue.main.async { let superView = view ?? UIApplication.shared.windows.filter({ $0.isKeyWindow }).first guard let superView = superView else { return } if let toastView = superView.findSubView(with: DLToastConfig.tag) as? DLToastView { toastView.removeFromSuperview() } let config = config config.containerRadius = icon == nil ? 4 : config.containerRadius let toastView = DLToastView(iconImage: icon, text: text, config: config, style: style) toastView.tag = DLToastConfig.tag toastView.completion = completion toastView.show(in: superView) } } /// 隐藏toast /// - Parameters: /// - view: 从哪个视图隐藏 /// - completion: 完成回调 public static func dismiss(for view: UIView? = nil, delay: TimeInterval = 0, completion: (() -> Void)? = nil) { DispatchQueue.main.async { let superView = view ?? UIApplication.shared.windows.filter({ $0.isKeyWindow }).first guard let toastView = superView?.findSubView(with: DLToastConfig.tag) as? DLToastView else { completion?() return } toastView.dismiss(delay: delay, completion) } } } // MARK: - UIView + Toast extension DLWrapper where Base: UIView { /// 显示loading /// - Parameters: /// - text: 文字 public func showLoading(text: String? = nil) { DLToast.showLoading(text: text, in: base) } /// 显示成功 /// - Parameters: /// - text: 文字 /// - view: 在哪个视图上显示,默认在window /// - completion: 完成后回调 public func showSuccess(text: String? = nil, completion: (() -> Void)? = nil) { DLToast.showSuccess(text: text, in: base, completion: completion) } /// 显示错误 /// - Parameters: /// - text: 文字 /// - completion: 完成后回调 public func showError(text: String? = nil, completion: (() -> Void)? = nil) { DLToast.showError(text: text, in: base, completion: completion) } /// 显示警告信息 /// - Parameters: /// - text: 文字 /// - completion: 完成后回调 public func showInfo(text: String? = nil, completion: (() -> Void)? = nil) { DLToast.showInfo(text: text, in: base, completion: completion) } /// 显示toast /// - Parameters: /// - text: 文字 /// - icon: 图标 /// - config: 配置信息 /// - style: 样式 /// - completion: 完成后回调 public func show(text: String, icon: UIImage? = nil, config: DLToastConfig = DLToastConfig(), style: DLToastConfig.Style = .none, completion: (() -> Void)? = nil) { DLToast.show(text: text, icon: icon, config: config, style: style, in: base, completion: completion) } /// 隐藏toast /// - Parameters: /// - completion: 完成回调 public func dismiss(delay: TimeInterval = 0, completion: (() -> Void)? = nil) { DLToast.dismiss(for: base, delay: delay, completion: completion) } } // MARK: - UIViewController + Toast extension DLWrapper where Base: UIViewController { /// 显示loading /// - Parameters: /// - text: 文字 public func showLoading(text: String? = nil) { DLToast.showLoading(text: text, in: base.view) } /// 显示成功 /// - Parameters: /// - text: 文字 /// - view: 在哪个视图上显示,默认在window /// - completion: 完成后回调 public func showSuccess(text: String? = nil, completion: (() -> Void)? = nil) { DLToast.showSuccess(text: text, in: base.view, completion: completion) } /// 显示错误 /// - Parameters: /// - text: 文字 /// - completion: 完成后回调 public func showError(text: String? = nil, completion: (() -> Void)? = nil) { DLToast.showError(text: text, in: base.view, completion: completion) } /// 显示警告信息 /// - Parameters: /// - text: 文字 /// - completion: 完成后回调 public func showInfo(text: String? = nil, completion: (() -> Void)? = nil) { DLToast.showInfo(text: text, in: base.view, completion: completion) } /// 显示toast /// - Parameters: /// - text: 文字 /// - icon: 图标 /// - config: 配置信息 /// - style: 样式 /// - completion: 完成后回调 public func show(text: String, icon: UIImage? = nil, config: DLToastConfig = DLToastConfig(), style: DLToastConfig.Style = .none, completion: (() -> Void)? = nil) { DLToast.show(text: text, icon: icon, config: config, style: style, in: base.view, completion: completion) } /// 隐藏toast /// - Parameters: /// - completion: 完成回调 public func dismiss(delay: TimeInterval = 0, completion: (() -> Void)? = nil) { DLToast.dismiss(for: base.view, delay: delay, completion: completion) } }