2019-07-31 17:40:39 +08:00
|
|
|
|
//
|
|
|
|
|
// ToastController.swift
|
|
|
|
|
// ProHUD
|
|
|
|
|
//
|
|
|
|
|
// Created by xaoxuu on 2019/7/31.
|
|
|
|
|
// Copyright © 2019 Titan Studio. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
2019-07-31 21:08:25 +08:00
|
|
|
|
import UIKit
|
2019-08-01 17:02:17 +08:00
|
|
|
|
import Inspire
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
public typealias Toast = ProHUD.Toast
|
|
|
|
|
|
2019-07-31 21:08:25 +08:00
|
|
|
|
public extension ProHUD {
|
|
|
|
|
class Toast: HUDController {
|
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
internal static var toasts = [Toast]()
|
|
|
|
|
|
2019-07-31 21:08:25 +08:00
|
|
|
|
public var window: UIWindow?
|
|
|
|
|
|
|
|
|
|
/// 图标
|
2019-08-05 11:18:25 +08:00
|
|
|
|
public lazy var imageView: UIImageView = {
|
2019-08-01 17:02:17 +08:00
|
|
|
|
let imgv = UIImageView()
|
|
|
|
|
imgv.contentMode = .scaleAspectFit
|
|
|
|
|
return imgv
|
|
|
|
|
}()
|
|
|
|
|
|
2020-06-19 13:50:17 +08:00
|
|
|
|
/// 文本区容器
|
|
|
|
|
public var textStack: StackContainer = {
|
|
|
|
|
let stack = StackContainer()
|
|
|
|
|
stack.spacing = cfg.toast.lineSpace
|
|
|
|
|
stack.alignment = .fill
|
|
|
|
|
return stack
|
|
|
|
|
}()
|
|
|
|
|
|
2019-08-01 17:02:17 +08:00
|
|
|
|
/// 标题
|
2019-08-05 11:18:25 +08:00
|
|
|
|
public lazy var titleLabel: UILabel = {
|
2019-08-01 17:02:17 +08:00
|
|
|
|
let lb = UILabel()
|
2019-08-03 17:48:37 +08:00
|
|
|
|
lb.textColor = cfg.primaryLabelColor
|
|
|
|
|
lb.font = cfg.toast.titleFont
|
2019-08-01 17:02:17 +08:00
|
|
|
|
lb.textAlignment = .justified
|
2019-08-03 17:48:37 +08:00
|
|
|
|
lb.numberOfLines = cfg.toast.titleMaxLines
|
2019-08-01 17:02:17 +08:00
|
|
|
|
return lb
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
/// 正文
|
2019-08-05 11:18:25 +08:00
|
|
|
|
public lazy var bodyLabel: UILabel = {
|
2019-08-01 17:02:17 +08:00
|
|
|
|
let lb = UILabel()
|
2019-08-03 17:48:37 +08:00
|
|
|
|
lb.textColor = cfg.secondaryLabelColor
|
|
|
|
|
lb.font = cfg.toast.bodyFont
|
2019-08-01 17:02:17 +08:00
|
|
|
|
lb.textAlignment = .justified
|
2019-08-03 17:48:37 +08:00
|
|
|
|
lb.numberOfLines = cfg.toast.bodyMaxLines
|
2019-08-01 17:02:17 +08:00
|
|
|
|
return lb
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}()
|
2019-08-01 17:02:17 +08:00
|
|
|
|
|
2019-08-03 16:22:50 +08:00
|
|
|
|
/// 背景层
|
2019-08-05 11:18:25 +08:00
|
|
|
|
public var backgroundView: UIVisualEffectView = {
|
2019-08-12 15:02:36 +08:00
|
|
|
|
let vev = createBlurView()
|
2019-08-03 16:22:50 +08:00
|
|
|
|
vev.layer.masksToBounds = true
|
2019-08-03 17:48:37 +08:00
|
|
|
|
vev.layer.cornerRadius = cfg.toast.cornerRadius
|
2019-08-03 16:22:50 +08:00
|
|
|
|
return vev
|
|
|
|
|
}()
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
2019-08-13 11:32:27 +08:00
|
|
|
|
/// 是否可以通过手势移除(向上滑出屏幕)
|
|
|
|
|
public var isRemovable = true
|
|
|
|
|
|
2019-07-31 21:08:25 +08:00
|
|
|
|
/// 视图模型
|
2019-08-12 15:02:36 +08:00
|
|
|
|
public var vm = ViewModel()
|
|
|
|
|
|
|
|
|
|
internal var maxY = CGFloat(0)
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
|
|
|
|
// MARK: 生命周期
|
|
|
|
|
|
|
|
|
|
/// 实例化
|
|
|
|
|
/// - Parameter scene: 场景
|
|
|
|
|
/// - Parameter title: 标题
|
|
|
|
|
/// - Parameter message: 内容
|
|
|
|
|
/// - Parameter icon: 图标
|
2019-08-12 20:20:07 +08:00
|
|
|
|
public convenience init(scene: Scene = .default, title: String? = nil, message: String? = nil, icon: UIImage? = nil, duration: TimeInterval? = nil, actions: ((Toast) -> Void)? = nil) {
|
2019-07-31 21:08:25 +08:00
|
|
|
|
self.init()
|
2019-08-12 15:02:36 +08:00
|
|
|
|
vm.vc = self
|
2019-08-01 17:02:17 +08:00
|
|
|
|
|
2019-08-12 15:02:36 +08:00
|
|
|
|
vm.scene = scene
|
|
|
|
|
vm.title = title
|
|
|
|
|
vm.message = message
|
|
|
|
|
vm.icon = icon
|
2019-08-12 17:59:40 +08:00
|
|
|
|
vm.duration = duration
|
2019-08-12 20:20:07 +08:00
|
|
|
|
actions?(self)
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
|
|
|
|
// 点击
|
|
|
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(privDidTapped(_:)))
|
|
|
|
|
view.addGestureRecognizer(tap)
|
|
|
|
|
// 拖动
|
|
|
|
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(privDidPan(_:)))
|
|
|
|
|
view.addGestureRecognizer(pan)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-08-12 15:02:36 +08:00
|
|
|
|
public override func viewDidLoad() {
|
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
|
|
|
|
cfg.toast.reloadData(self)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-08-05 11:18:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - 实例函数
|
2019-08-08 11:09:22 +08:00
|
|
|
|
public extension Toast {
|
2019-08-05 11:18:25 +08:00
|
|
|
|
|
|
|
|
|
/// 推入屏幕
|
2019-08-08 11:09:22 +08:00
|
|
|
|
@discardableResult func push() -> Toast {
|
|
|
|
|
let config = cfg.toast
|
|
|
|
|
let isNew: Bool
|
|
|
|
|
if self.window == nil {
|
2020-06-19 10:48:47 +08:00
|
|
|
|
let window = UIWindow(frame: .zero)
|
|
|
|
|
self.window = window
|
2020-06-15 15:11:44 +08:00
|
|
|
|
if #available(iOS 13.0, *) {
|
2020-06-19 10:48:47 +08:00
|
|
|
|
window.windowScene = cfg.windowScene ?? UIApplication.shared.windows.last?.windowScene
|
2020-06-15 15:11:44 +08:00
|
|
|
|
} else {
|
|
|
|
|
// Fallback on earlier versions
|
|
|
|
|
}
|
2020-06-19 19:13:33 +08:00
|
|
|
|
window.windowLevel = .proToast
|
2020-06-19 10:48:47 +08:00
|
|
|
|
window.backgroundColor = .clear
|
|
|
|
|
window.layer.shadowRadius = 8
|
|
|
|
|
window.layer.shadowOffset = .init(width: 0, height: 5)
|
|
|
|
|
window.layer.shadowOpacity = 0.2
|
|
|
|
|
window.isHidden = false
|
2019-08-08 11:09:22 +08:00
|
|
|
|
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)
|
|
|
|
|
}
|
2019-08-12 15:02:36 +08:00
|
|
|
|
Toast.privUpdateToastsLayout()
|
2019-08-08 11:09:22 +08:00
|
|
|
|
if isNew {
|
|
|
|
|
window.transform = .init(translationX: 0, y: -window.frame.maxY)
|
|
|
|
|
UIView.animateForToast {
|
|
|
|
|
window.transform = .identity
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
view.layoutIfNeeded()
|
|
|
|
|
}
|
|
|
|
|
return self
|
2019-08-05 11:18:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 弹出屏幕
|
|
|
|
|
func pop() {
|
2019-08-12 15:02:36 +08:00
|
|
|
|
Toast.pop(self)
|
2019-08-05 11:18:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-08-12 15:02:36 +08:00
|
|
|
|
/// 更新
|
|
|
|
|
/// - Parameter callback: 回调
|
|
|
|
|
func update(_ callback: ((inout ViewModel) -> Void)? = nil) {
|
|
|
|
|
callback?(&vm)
|
|
|
|
|
cfg.toast.reloadData(self)
|
2019-08-05 11:18:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 点击事件
|
|
|
|
|
/// - Parameter callback: 事件回调
|
2019-08-12 19:02:33 +08:00
|
|
|
|
func didTapped(_ callback: (() -> Void)?) {
|
2019-08-12 15:02:36 +08:00
|
|
|
|
vm.tapCallback = callback
|
2019-08-05 11:18:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-13 09:14:30 +08:00
|
|
|
|
/// 脉冲效果
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-19 19:13:33 +08:00
|
|
|
|
extension Toast: RotateAnimation {
|
|
|
|
|
|
|
|
|
|
}
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
2019-08-12 15:02:36 +08:00
|
|
|
|
// MARK: - 实例管理器
|
2019-08-08 11:09:22 +08:00
|
|
|
|
public extension Toast {
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
/// 推入屏幕
|
2019-08-05 11:18:25 +08:00
|
|
|
|
/// - Parameter toast: 场景
|
|
|
|
|
/// - Parameter title: 标题
|
|
|
|
|
/// - Parameter message: 内容
|
2019-08-08 11:09:22 +08:00
|
|
|
|
/// - Parameter actions: 更多操作
|
2019-08-13 11:32:27 +08:00
|
|
|
|
@discardableResult class func push(scene: ProHUD.Scene = .default, title: String? = nil, message: String? = nil, duration: TimeInterval? = nil, _ actions: ((Toast) -> Void)? = nil) -> Toast {
|
2019-08-12 17:59:40 +08:00
|
|
|
|
return Toast(scene: scene, title: title, message: message, duration: duration, actions: actions).push()
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-13 09:14:30 +08:00
|
|
|
|
/// 查找指定的实例
|
2019-08-05 11:18:25 +08:00
|
|
|
|
/// - Parameter identifier: 标识
|
2019-08-12 20:20:07 +08:00
|
|
|
|
class func find(_ identifier: String?) -> [Toast] {
|
2019-07-31 21:08:25 +08:00
|
|
|
|
var tt = [Toast]()
|
|
|
|
|
for t in toasts {
|
2019-08-12 20:20:07 +08:00
|
|
|
|
if t.identifier == identifier {
|
2019-07-31 21:08:25 +08:00
|
|
|
|
tt.append(t)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-08 11:09:22 +08:00
|
|
|
|
return tt
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-12 20:20:07 +08:00
|
|
|
|
/// 查找指定的实例
|
|
|
|
|
/// - Parameter identifier: 标识
|
|
|
|
|
/// - Parameter last: 已经存在(获取最后一个)
|
|
|
|
|
/// - Parameter none: 不存在
|
|
|
|
|
class func find(_ identifier: String?, last: ((Toast) -> Void)? = nil, none: (() -> Void)? = nil) {
|
|
|
|
|
if let t = find(identifier).last {
|
|
|
|
|
last?(t)
|
|
|
|
|
} else {
|
|
|
|
|
none?()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
/// 弹出屏幕
|
2019-08-05 11:18:25 +08:00
|
|
|
|
/// - Parameter toast: 实例
|
2019-08-08 11:09:22 +08:00
|
|
|
|
class func pop(_ toast: Toast) {
|
2019-08-12 19:02:33 +08:00
|
|
|
|
toast.willDisappearCallback?()
|
2019-08-12 15:02:36 +08:00
|
|
|
|
if toasts.count > 1 {
|
|
|
|
|
for (i, t) in toasts.enumerated() {
|
|
|
|
|
if t == toast {
|
|
|
|
|
toasts.remove(at: i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
privUpdateToastsLayout()
|
|
|
|
|
} else if toasts.count == 1 {
|
|
|
|
|
toasts.removeAll()
|
|
|
|
|
} else {
|
2020-06-19 10:54:55 +08:00
|
|
|
|
debug("‼️代码漏洞:已经没有toast了")
|
2019-08-12 15:02:36 +08:00
|
|
|
|
}
|
|
|
|
|
UIView.animateForToast(animations: {
|
|
|
|
|
toast.window?.transform = .init(translationX: 0, y: -20-toast.maxY)
|
|
|
|
|
}) { (done) in
|
|
|
|
|
toast.view.removeFromSuperview()
|
|
|
|
|
toast.removeFromParent()
|
|
|
|
|
toast.window = nil
|
|
|
|
|
}
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
/// 弹出屏幕
|
|
|
|
|
/// - Parameter identifier: 指定实例的标识
|
|
|
|
|
class func pop(_ identifier: String?) {
|
2019-08-12 20:20:07 +08:00
|
|
|
|
for t in find(identifier) {
|
2019-08-08 11:09:22 +08:00
|
|
|
|
t.pop()
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-05 11:18:25 +08:00
|
|
|
|
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-12 15:02:36 +08:00
|
|
|
|
// MARK: - 创建和设置
|
|
|
|
|
fileprivate var willprivUpdateToastsLayout: DispatchWorkItem?
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
fileprivate extension Toast {
|
2019-07-31 21:08:25 +08:00
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
/// 点击事件
|
|
|
|
|
/// - Parameter sender: 手势
|
|
|
|
|
@objc func privDidTapped(_ sender: UITapGestureRecognizer) {
|
2019-08-12 15:02:36 +08:00
|
|
|
|
vm.tapCallback?()
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-08 11:09:22 +08:00
|
|
|
|
/// 拖拽事件
|
|
|
|
|
/// - Parameter sender: 手势
|
|
|
|
|
@objc func privDidPan(_ sender: UIPanGestureRecognizer) {
|
2019-08-12 15:02:36 +08:00
|
|
|
|
vm.durationBlock?.cancel()
|
2019-08-08 11:09:22 +08:00
|
|
|
|
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)
|
2019-08-13 11:32:27 +08:00
|
|
|
|
if isRemovable == true && (((window?.frame.origin.y ?? 0) < 0 && v.y < 0) || v.y < -1200) {
|
2019-08-08 11:09:22 +08:00
|
|
|
|
// 移除
|
|
|
|
|
self.pop()
|
|
|
|
|
} else {
|
|
|
|
|
UIView.animateForToast(animations: {
|
|
|
|
|
self.window?.transform = .identity
|
|
|
|
|
}) { (done) in
|
2019-08-12 15:02:36 +08:00
|
|
|
|
let d = self.vm.duration
|
|
|
|
|
self.vm.duration = d
|
2019-08-08 11:09:22 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-08-12 15:02:36 +08:00
|
|
|
|
class func privUpdateToastsLayout() {
|
2019-08-01 20:22:57 +08:00
|
|
|
|
func f() {
|
|
|
|
|
let top = Inspire.shared.screen.updatedSafeAreaInsets.top
|
|
|
|
|
for (i, e) in toasts.enumerated() {
|
2019-08-03 17:48:37 +08:00
|
|
|
|
let config = cfg.toast
|
2019-08-01 20:22:57 +08:00
|
|
|
|
if let window = e.window {
|
|
|
|
|
var y = window.frame.origin.y
|
|
|
|
|
if i == 0 {
|
|
|
|
|
if isPortrait {
|
|
|
|
|
y = top
|
|
|
|
|
} else {
|
|
|
|
|
y = config.margin
|
|
|
|
|
}
|
2019-08-01 17:02:17 +08:00
|
|
|
|
} else {
|
2019-08-01 20:22:57 +08:00
|
|
|
|
let lastY = toasts[i-1].window?.frame.maxY ?? .zero
|
|
|
|
|
y = lastY + config.margin
|
|
|
|
|
}
|
2019-08-12 15:02:36 +08:00
|
|
|
|
e.maxY = y + window.frame.size.height
|
2019-08-01 20:22:57 +08:00
|
|
|
|
UIView.animateForToast {
|
2019-08-12 15:02:36 +08:00
|
|
|
|
window.frame.origin.y = y
|
2019-08-01 17:02:17 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-08-12 15:02:36 +08:00
|
|
|
|
willprivUpdateToastsLayout?.cancel()
|
|
|
|
|
willprivUpdateToastsLayout = DispatchWorkItem(block: {
|
2019-08-01 20:22:57 +08:00
|
|
|
|
f()
|
|
|
|
|
})
|
2019-08-12 15:02:36 +08:00
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now()+0.001, execute: willprivUpdateToastsLayout!)
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
2019-08-12 15:02:36 +08:00
|
|
|
|
|
2019-07-31 21:08:25 +08:00
|
|
|
|
}
|
|
|
|
|
|