ProHUD/Source/Toast/ToastController.swift

351 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ToastController.swift
// ProHUD
//
// Created by xaoxuu on 2019/7/31.
// Copyright © 2019 Titan Studio. All rights reserved.
//
import UIKit
import Inspire
public typealias Toast = ProHUD.Toast
public extension ProHUD {
class Toast: HUDController {
internal static var toasts = [Toast]()
public var window: UIWindow?
///
public lazy var imageView: UIImageView = {
let imgv = UIImageView()
imgv.contentMode = .scaleAspectFit
return imgv
}()
///
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()
internal var maxY = CGFloat(0)
// MARK:
///
/// - Parameter scene:
/// - Parameter title:
/// - Parameter message:
/// - Parameter icon:
public convenience init(scene: Scene = .default, title: String? = nil, message: String? = nil, icon: UIImage? = nil, duration: TimeInterval? = nil, actions: ((Toast) -> Void)? = nil) {
self.init()
vm.vc = self
vm.scene = scene
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 {
let w = UIWindow(frame: .zero)
self.window = w
w.windowLevel = UIWindow.Level(5000)
w.backgroundColor = .clear
w.layer.shadowRadius = 8
w.layer.shadowOffset = .init(width: 0, height: 5)
w.layer.shadowOpacity = 0.2
w.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.sizewindow.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()
}
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
}
///
/// - Parameter flag:
func rotate(_ flag: Bool = true) {
if flag {
DispatchQueue.main.async {
let ani = CABasicAnimation(keyPath: "transform.rotation.z")
ani.toValue = Double.pi * 2.0
ani.duration = 3
ani.repeatCount = 10000
self.imageView.layer.add(ani, forKey: "rotationAnimation")
}
} else {
imageView.layer.removeAllAnimations()
}
}
///
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: -
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()
}
///
/// - 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:
/// - Parameter none:
class func find(_ identifier: String?, last: ((Toast) -> Void)? = nil, none: (() -> Void)? = nil) {
if let t = find(identifier).last {
last?(t)
} else {
none?()
}
}
///
/// - Parameter toast:
class func pop(_ toast: Toast) {
toast.willDisappearCallback?()
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 {
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
}
}
///
/// - 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) {
vm.tapCallback?()
}
///
/// - 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 = Inspire.shared.screen.updatedSafeAreaInsets.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!)
}
}