mirror of https://github.com/xaoxuu/ProHUD
368 lines
11 KiB
Swift
368 lines
11 KiB
Swift
//
|
||
// ToastController.swift
|
||
// ProHUD
|
||
//
|
||
// Created by xaoxuu on 2019/7/31.
|
||
// Copyright © 2019 Titan Studio. All rights reserved.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
public typealias Toast = ProHUD.Toast
|
||
|
||
public extension ProHUD {
|
||
class Toast: HUDController {
|
||
|
||
static var toasts = [Toast]()
|
||
|
||
public var window: UIWindow?
|
||
|
||
/// 图标
|
||
public lazy var imageView: UIImageView = {
|
||
let imgv = UIImageView()
|
||
imgv.contentMode = .scaleAspectFit
|
||
return imgv
|
||
}()
|
||
|
||
/// 文本区容器
|
||
public var textStack: StackContainer = {
|
||
let stack = StackContainer()
|
||
stack.spacing = cfg.toast.lineSpace
|
||
stack.alignment = .fill
|
||
return stack
|
||
}()
|
||
|
||
/// 标题
|
||
public lazy var titleLabel: UILabel = {
|
||
let lb = UILabel()
|
||
lb.textColor = cfg.primaryLabelColor
|
||
lb.font = cfg.toast.titleFont
|
||
lb.textAlignment = .justified
|
||
lb.numberOfLines = cfg.toast.titleMaxLines
|
||
return lb
|
||
}()
|
||
|
||
/// 正文
|
||
public lazy var bodyLabel: UILabel = {
|
||
let lb = UILabel()
|
||
lb.textColor = cfg.secondaryLabelColor
|
||
lb.font = cfg.toast.bodyFont
|
||
lb.textAlignment = .justified
|
||
lb.numberOfLines = cfg.toast.bodyMaxLines
|
||
return lb
|
||
}()
|
||
|
||
/// 背景层
|
||
public var backgroundView: UIVisualEffectView = {
|
||
let vev = createBlurView()
|
||
vev.layer.masksToBounds = true
|
||
vev.layer.cornerRadius = cfg.toast.cornerRadius
|
||
return vev
|
||
}()
|
||
|
||
/// 是否可以通过手势移除(向上滑出屏幕)
|
||
public var isRemovable = true
|
||
|
||
/// 视图模型
|
||
public var vm = ViewModel()
|
||
|
||
var maxY = CGFloat(0)
|
||
|
||
// MARK: 生命周期
|
||
|
||
/// 实例化
|
||
/// - Parameter scene: 场景
|
||
/// - Parameter title: 标题
|
||
/// - Parameter message: 内容
|
||
/// - Parameter icon: 图标
|
||
public convenience init(scene: Scene?, title: String? = nil, message: String? = nil, icon: UIImage? = nil, duration: TimeInterval? = nil, actions: ((Toast) -> Void)? = nil) {
|
||
self.init()
|
||
vm.vc = self
|
||
|
||
vm.scene = scene ?? .default
|
||
vm.title = title
|
||
vm.message = message
|
||
vm.icon = icon
|
||
vm.duration = duration
|
||
actions?(self)
|
||
|
||
// 点击
|
||
let tap = UITapGestureRecognizer(target: self, action: #selector(privDidTapped(_:)))
|
||
view.addGestureRecognizer(tap)
|
||
// 拖动
|
||
let pan = UIPanGestureRecognizer(target: self, action: #selector(privDidPan(_:)))
|
||
view.addGestureRecognizer(pan)
|
||
|
||
}
|
||
|
||
|
||
public override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
|
||
cfg.toast.reloadData(self)
|
||
|
||
}
|
||
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - 实例函数
|
||
public extension Toast {
|
||
|
||
/// 推入屏幕
|
||
@discardableResult func push() -> Toast {
|
||
let config = cfg.toast
|
||
let isNew: Bool
|
||
if self.window == nil {
|
||
willAppearCallback?()
|
||
let window = UIWindow(frame: .zero)
|
||
self.window = window
|
||
if #available(iOS 13.0, *) {
|
||
window.windowScene = cfg.windowScene ?? .currentWindowScene
|
||
}
|
||
window.windowLevel = .proToast
|
||
window.backgroundColor = .clear
|
||
window.layer.shadowRadius = 8
|
||
window.layer.shadowOffset = .init(width: 0, height: 5)
|
||
window.layer.shadowOpacity = 0.2
|
||
window.isHidden = false
|
||
isNew = true
|
||
} else {
|
||
isNew = false
|
||
}
|
||
|
||
let window = self.window!
|
||
// background & frame
|
||
// 设定正确的宽度,更新子视图
|
||
let width = CGFloat.minimum(UIScreen.main.bounds.width - 2*config.margin, config.maxWidth)
|
||
view.frame.size = CGSize(width: width, height: 800)
|
||
titleLabel.sizeToFit()
|
||
bodyLabel.sizeToFit()
|
||
view.layoutIfNeeded()
|
||
// 更新子视图之后获取正确的高度
|
||
var height = CGFloat(0)
|
||
for v in self.view.subviews {
|
||
height = CGFloat.maximum(v.frame.maxY, height)
|
||
}
|
||
height += config.padding
|
||
// 应用到frame
|
||
window.frame = CGRect(x: (UIScreen.main.bounds.width - width) / 2, y: 0, width: width, height: height)
|
||
backgroundView.frame.size = window.frame.size
|
||
window.insertSubview(backgroundView, at: 0)
|
||
window.rootViewController = self // 此时toast.view.frame.size会自动更新为window.frame.size
|
||
// 根据在屏幕中的顺序,确定y坐标
|
||
if Toast.toasts.contains(self) == false {
|
||
Toast.toasts.append(self)
|
||
}
|
||
Toast.privUpdateToastsLayout()
|
||
if isNew {
|
||
window.transform = .init(translationX: 0, y: -window.frame.maxY)
|
||
UIView.animateForToast {
|
||
window.transform = .identity
|
||
}
|
||
} else {
|
||
view.layoutIfNeeded()
|
||
}
|
||
didAppearCallback?()
|
||
return self
|
||
}
|
||
|
||
/// 弹出屏幕
|
||
func pop() {
|
||
Toast.pop(self)
|
||
}
|
||
|
||
|
||
/// 更新
|
||
/// - Parameter callback: 回调
|
||
func update(_ callback: ((inout ViewModel) -> Void)? = nil) {
|
||
callback?(&vm)
|
||
cfg.toast.reloadData(self)
|
||
}
|
||
|
||
/// 点击事件
|
||
/// - Parameter callback: 事件回调
|
||
func didTapped(_ callback: (() -> Void)?) {
|
||
vm.tapCallback = callback
|
||
}
|
||
|
||
/// 脉冲效果
|
||
func pulse() {
|
||
DispatchQueue.main.async {
|
||
UIView.animate(withDuration: 0.2, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.allowUserInteraction, .curveEaseOut], animations: {
|
||
self.window?.transform = .init(scaleX: 1.04, y: 1.04)
|
||
}) { (done) in
|
||
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1, options: [.allowUserInteraction, .curveEaseIn], animations: {
|
||
self.window?.transform = .identity
|
||
}) { (done) in
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: 支持加载动画
|
||
extension Toast: LoadingRotateAnimation {}
|
||
|
||
// MARK: - 实例管理器
|
||
public extension Toast {
|
||
|
||
/// 创建实例并推入屏幕
|
||
/// - Parameter toast: 场景
|
||
/// - Parameter title: 标题
|
||
/// - Parameter message: 内容
|
||
/// - Parameter actions: 更多操作
|
||
@discardableResult class func push(scene: ProHUD.Scene = .default, title: String? = nil, message: String? = nil, duration: TimeInterval? = nil, _ actions: ((Toast) -> Void)? = nil) -> Toast {
|
||
return Toast(scene: scene, title: title, message: message, duration: duration, actions: actions).push()
|
||
}
|
||
|
||
/// 创建实例并推入屏幕
|
||
/// - Parameters:
|
||
/// - identifier: 唯一标识
|
||
/// - toast: 实例对象
|
||
/// - Returns: 回调
|
||
@discardableResult class func push(_ identifier: String, scene: ProHUD.Scene? = nil, instance: ((Toast) -> Void)? = nil) -> Toast {
|
||
if let t = find(identifier).last {
|
||
if let s = scene, s != t.vm.scene {
|
||
t.update { (vm) in
|
||
vm.scene = s
|
||
}
|
||
}
|
||
instance?(t)
|
||
return t
|
||
} else {
|
||
return Toast(scene: scene) { (tt) in
|
||
tt.identifier = identifier
|
||
instance?(tt)
|
||
}.push()
|
||
}
|
||
}
|
||
|
||
/// 查找指定的实例
|
||
/// - Parameter identifier: 标识
|
||
class func find(_ identifier: String) -> [Toast] {
|
||
var tt = [Toast]()
|
||
for t in toasts {
|
||
if t.identifier == identifier {
|
||
tt.append(t)
|
||
}
|
||
}
|
||
return tt
|
||
}
|
||
|
||
/// 查找指定的实例
|
||
/// - Parameter identifier: 标识
|
||
/// - Parameter last: 已经存在(获取最后一个)
|
||
class func find(_ identifier: String, last: @escaping (Toast) -> Void) {
|
||
if let t = find(identifier).last {
|
||
last(t)
|
||
}
|
||
}
|
||
|
||
/// 弹出屏幕
|
||
/// - Parameter toast: 实例
|
||
class func pop(_ toast: Toast) {
|
||
toast.willDisappearCallback?()
|
||
if toasts.count > 1 {
|
||
toasts.removeAll { $0 == toast }
|
||
privUpdateToastsLayout()
|
||
} else if toasts.count == 1 {
|
||
toasts.removeAll()
|
||
} else {
|
||
debug("‼️代码漏洞:已经没有toast了")
|
||
}
|
||
UIView.animateForToast(animations: {
|
||
toast.window?.transform = .init(translationX: 0, y: -20-toast.maxY)
|
||
}) { (done) in
|
||
toast.view.removeFromSuperview()
|
||
toast.removeFromParent()
|
||
toast.window = nil
|
||
toast.didDisappearCallback?()
|
||
}
|
||
}
|
||
|
||
/// 弹出屏幕
|
||
/// - Parameter identifier: 指定实例的标识
|
||
class func pop(_ identifier: String) {
|
||
for t in find(identifier) {
|
||
t.pop()
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - 创建和设置
|
||
fileprivate var willprivUpdateToastsLayout: DispatchWorkItem?
|
||
|
||
fileprivate extension Toast {
|
||
|
||
/// 点击事件
|
||
/// - Parameter sender: 手势
|
||
@objc func privDidTapped(_ sender: UITapGestureRecognizer) {
|
||
if let cb = vm.tapCallback {
|
||
cb()
|
||
} else {
|
||
pulse()
|
||
}
|
||
}
|
||
|
||
/// 拖拽事件
|
||
/// - Parameter sender: 手势
|
||
@objc func privDidPan(_ sender: UIPanGestureRecognizer) {
|
||
vm.durationBlock?.cancel()
|
||
let point = sender.translation(in: sender.view)
|
||
window?.transform = .init(translationX: 0, y: point.y)
|
||
if sender.state == .recognized {
|
||
let v = sender.velocity(in: sender.view)
|
||
if isRemovable == true && (((window?.frame.origin.y ?? 0) < 0 && v.y < 0) || v.y < -1200) {
|
||
// 移除
|
||
self.pop()
|
||
} else {
|
||
UIView.animateForToast(animations: {
|
||
self.window?.transform = .identity
|
||
}) { (done) in
|
||
let d = self.vm.duration
|
||
self.vm.duration = d
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
class func privUpdateToastsLayout() {
|
||
func f() {
|
||
let top = ProHUD.safeAreaInsets.top
|
||
for (i, e) in toasts.enumerated() {
|
||
let config = cfg.toast
|
||
if let window = e.window {
|
||
var y = window.frame.origin.y
|
||
if i == 0 {
|
||
if isPortrait {
|
||
y = top
|
||
} else {
|
||
y = config.margin
|
||
}
|
||
} else {
|
||
let lastY = toasts[i-1].window?.frame.maxY ?? .zero
|
||
y = lastY + config.margin
|
||
}
|
||
e.maxY = y + window.frame.size.height
|
||
UIView.animateForToast {
|
||
window.frame.origin.y = y
|
||
}
|
||
}
|
||
}
|
||
}
|
||
willprivUpdateToastsLayout?.cancel()
|
||
willprivUpdateToastsLayout = DispatchWorkItem(block: {
|
||
f()
|
||
})
|
||
DispatchQueue.main.asyncAfter(deadline: .now()+0.001, execute: willprivUpdateToastsLayout!)
|
||
}
|
||
|
||
}
|
||
|