Tusker/Tusker/Views/Toast/ToastView.swift

224 lines
8.2 KiB
Swift

//
// ToastView.swift
// ToastView
//
// Created by Shadowfacts on 8/14/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
class ToastView: UIView {
let configuration: ToastConfiguration
private var shrinkAnimator: UIViewPropertyAnimator?
private var recognizedGesture = false
private var offscreenTranslation: CGFloat {
var translation = bounds.height + configuration.edgeSpacing
if configuration.edge == .bottom {
translation += superview?.safeAreaInsets.bottom ?? 0
} else {
translation += superview?.safeAreaInsets.top ?? 0
translation *= -1
}
return translation
}
init(configuration: ToastConfiguration) {
precondition(configuration.edge != .automatic)
self.configuration = configuration
super.init(frame: .zero)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
backgroundColor = .systemBlue
layer.shadowColor = UIColor.black.cgColor
layer.shadowRadius = 5
layer.shadowOffset = CGSize(width: 0, height: 2.5)
layer.shadowOpacity = 0.5
layer.masksToBounds = false
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .horizontal
stack.spacing = 8
if let name = configuration.systemImageName {
let imageView = UIImageView(image: UIImage(systemName: name))
imageView.tintColor = .white
imageView.contentMode = .scaleAspectFit
stack.addArrangedSubview(imageView)
}
let titleLabel = UILabel()
titleLabel.text = configuration.title
titleLabel.textColor = .white
titleLabel.font = .boldSystemFont(ofSize: 14)
titleLabel.adjustsFontSizeToFitWidth = true
if let subtitle = configuration.subtitle {
let subtitleLabel = UILabel()
subtitleLabel.text = subtitle
subtitleLabel.textColor = .white
subtitleLabel.numberOfLines = 0
subtitleLabel.font = .systemFont(ofSize: 14)
let vStack = UIStackView(arrangedSubviews: [
titleLabel,
subtitleLabel
])
vStack.axis = .vertical
vStack.spacing = 4
stack.addArrangedSubview(vStack)
} else {
stack.addArrangedSubview(titleLabel)
}
if let actionTitle = configuration.actionTitle {
let actionLabel = UILabel()
actionLabel.text = actionTitle
actionLabel.font = .boldSystemFont(ofSize: 16)
actionLabel.textColor = .white
stack.addArrangedSubview(actionLabel)
}
addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
stack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
])
let pan = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
addGestureRecognizer(pan)
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(32, bounds.height / 2)
layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: layer.cornerRadius, cornerHeight: layer.cornerRadius, transform: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
recognizedGesture = false
shrinkAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) {
self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
}
shrinkAnimator?.startAnimation(afterDelay: 0.1)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if !recognizedGesture {
guard let shrinkAnimator = shrinkAnimator else {
return
}
shrinkAnimator.stopAnimation(true)
UIView.animate(withDuration: 0.1, delay: 0, options: .curveEaseInOut) {
self.transform = .identity
}
configuration.action?(self)
}
shrinkAnimator = nil
}
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self).y
let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0)
switch recognizer.state {
case .began:
recognizedGesture = true
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
self.transform = .identity
}
break
case .changed:
var distance: CGFloat
if isDraggingAwayFromDismissalEdge {
distance = sqrt(abs(translation))
if configuration.edge == .bottom {
distance *= -1
}
} else {
distance = translation
}
transform = CGAffineTransform(translationX: 0, y: distance)
case .ended, .cancelled:
let velocity = recognizer.velocity(in: self).y
let distance = isDraggingAwayFromDismissalEdge ? sqrt(abs(translation)) : translation
let minDismissalDistance = configuration.edgeSpacing + bounds.height / 2
let dismissDueToDistance = configuration.edge == .bottom ? distance > minDismissalDistance : -distance > minDismissalDistance
let minDismissalVelocity: CGFloat = 250
let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity
if dismissDueToDistance || dismissDueToVelocity {
if abs(translation) < abs(offscreenTranslation) {
let distanceLeft = offscreenTranslation - translation
let duration = 1 / TimeInterval(max(velocity, minDismissalVelocity) / distanceLeft)
UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction) {
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
} completion: { (_) in
self.removeFromSuperview()
}
} else {
self.removeFromSuperview()
}
} else {
let duration = 0.5
let velocity = distance * duration
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) {
self.transform = .identity
}
}
default:
break
}
}
func dismissToast(animated: Bool) {
guard animated else {
removeFromSuperview()
return
}
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
} completion: { (_) in
self.removeFromSuperview()
}
}
func animateAppearance() {
self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation)
let duration = 0.5
let velocity = 0.5
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) {
self.transform = .identity
}
}
}