ProHUD/Source/Toast/ToastController.swift

368 lines
11 KiB
Swift
Raw Permalink Normal View History

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-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 {
2021-07-20 16:04:39 +08:00
static var toasts = [Toast]()
2019-08-08 11:09:22 +08:00
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()
2021-07-20 16:04:39 +08:00
var maxY = CGFloat(0)
2019-07-31 21:08:25 +08:00
// MARK:
///
/// - Parameter scene:
/// - Parameter title:
/// - Parameter message:
/// - Parameter icon:
2020-06-23 20:16:12 +08:00
public convenience init(scene: Scene?, 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
2020-06-23 20:16:12 +08:00
vm.scene = scene ?? .default
2019-08-12 15:02:36 +08:00
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-22 13:27:54 +08:00
willAppearCallback?()
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, *) {
2021-07-20 16:04:39 +08:00
window.windowScene = cfg.windowScene ?? .currentWindowScene
2020-06-15 15:11:44 +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.sizewindow.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()
}
2020-06-22 13:27:54 +08:00
didAppearCallback?()
2019-08-08 11:09:22 +08:00
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-22 13:27:54 +08:00
// MARK:
extension Toast: LoadingRotateAnimation {}
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-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
}
///
/// - Parameters:
/// - identifier:
/// - toast:
/// - Returns:
2020-06-23 20:16:12 +08:00
@discardableResult class func push(_ identifier: String, scene: ProHUD.Scene? = nil, instance: ((Toast) -> Void)? = nil) -> Toast {
if let t = find(identifier).last {
2020-06-23 20:16:12 +08:00
if let s = scene, s != t.vm.scene {
t.update { (vm) in
vm.scene = s
}
}
instance?(t)
return t
} else {
2020-06-23 20:16:12 +08:00
return Toast(scene: scene) { (tt) in
tt.identifier = identifier
2020-06-23 20:16:12 +08:00
instance?(tt)
}.push()
}
}
2019-08-13 09:14:30 +08:00
///
2019-08-05 11:18:25 +08:00
/// - Parameter identifier:
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:
class func find(_ identifier: String, last: @escaping (Toast) -> Void) {
2019-08-12 20:20:07 +08:00
if let t = find(identifier).last {
last(t)
2019-08-12 20:20:07 +08:00
}
}
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 {
2021-07-20 16:04:39 +08:00
toasts.removeAll { $0 == toast }
2019-08-12 15:02:36 +08:00
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
2020-06-22 13:27:54 +08:00
toast.didDisappearCallback?()
2019-08-12 15:02:36 +08:00
}
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) {
if let cb = vm.tapCallback {
cb()
} else {
pulse()
}
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() {
2020-06-22 10:14:47 +08:00
let top = ProHUD.safeAreaInsets.top
2019-08-01 20:22:57 +08:00
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
}