483 lines
16 KiB
Swift
483 lines
16 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|