ProHUD/Source/Toast/ToastController.swift

374 lines
12 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
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 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()
internal 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 ?? UIApplication.shared.windows.first?.windowScene
} else {
// Fallback on earlier versions
}
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.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()
}
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 {
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
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!)
}
}