Add toast system
This commit is contained in:
parent
ba0d179de5
commit
7f4bf52050
|
@ -141,6 +141,9 @@
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
||||||
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||||
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
|
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
|
||||||
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.swift */; };
|
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.swift */; };
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
|
||||||
|
@ -544,6 +547,9 @@
|
||||||
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
||||||
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
||||||
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
|
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
|
||||||
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinStatusActivity.swift; sourceTree = "<group>"; };
|
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinStatusActivity.swift; sourceTree = "<group>"; };
|
||||||
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
|
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1263,6 +1269,16 @@
|
||||||
path = Transitions;
|
path = Transitions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D64AAE8F26C80DB600FC57FB /* Toast */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */,
|
||||||
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */,
|
||||||
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Toast;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D65A37F221472F300087646E /* Frameworks */ = {
|
D65A37F221472F300087646E /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1483,6 +1499,7 @@
|
||||||
D623A53B2635F4E20095BD04 /* Poll */,
|
D623A53B2635F4E20095BD04 /* Poll */,
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
|
D64AAE8F26C80DB600FC57FB /* Toast */,
|
||||||
D6420AEB26BED17500ED8175 /* Timeline Description Cell */,
|
D6420AEB26BED17500ED8175 /* Timeline Description Cell */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
|
@ -2067,6 +2084,7 @@
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||||
|
@ -2148,6 +2166,7 @@
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
|
@ -2162,6 +2181,7 @@
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
||||||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
||||||
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||||
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// ToastConfiguration.swift
|
||||||
|
// ToastConfiguration
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ToastConfiguration {
|
||||||
|
var systemImageName: String?
|
||||||
|
var title: String
|
||||||
|
var subtitle: String?
|
||||||
|
var actionTitle: String?
|
||||||
|
var action: ((ToastView) -> Void)?
|
||||||
|
var edgeSpacing: CGFloat = 8
|
||||||
|
var edge: Edge = .automatic
|
||||||
|
var dismissOnScroll = true
|
||||||
|
|
||||||
|
init(title: String) {
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Edge: Equatable {
|
||||||
|
case top
|
||||||
|
case bottom
|
||||||
|
/// Determines edge based on the current device. Bottom on iPhone, top on iPad/Mac.
|
||||||
|
case automatic
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// ToastableViewController.swift
|
||||||
|
// ToastableViewController
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ToastableViewController: UIViewController {
|
||||||
|
|
||||||
|
var toastParentView: UIView { get }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentToastKey = "Tusker_currentToast"
|
||||||
|
|
||||||
|
extension ToastableViewController {
|
||||||
|
|
||||||
|
private(set) var currentToast: ToastView? {
|
||||||
|
get {
|
||||||
|
let holder = objc_getAssociatedObject(self, ¤tToastKey) as? WeakHolder<ToastView>
|
||||||
|
return holder?.object
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let newValue = newValue {
|
||||||
|
objc_setAssociatedObject(self, ¤tToastKey, WeakHolder(object: newValue), .OBJC_ASSOCIATION_RETAIN)
|
||||||
|
} else {
|
||||||
|
objc_setAssociatedObject(self, ¤tToastKey, nil, .OBJC_ASSOCIATION_RETAIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toastParentView: UIView { view }
|
||||||
|
|
||||||
|
func showToast(configuration config: ToastConfiguration, animated: Bool) {
|
||||||
|
currentToast?.dismissToast(animated: false)
|
||||||
|
|
||||||
|
var config = config
|
||||||
|
config.edge = effectiveEdge(edge: config.edge)
|
||||||
|
|
||||||
|
let toast = ToastView(configuration: config)
|
||||||
|
currentToast = toast
|
||||||
|
|
||||||
|
let parentSafeArea = toastParentView.safeAreaLayoutGuide
|
||||||
|
toast.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
toastParentView.addSubview(toast)
|
||||||
|
|
||||||
|
let yConstraint: NSLayoutConstraint
|
||||||
|
switch config.edge {
|
||||||
|
case .top:
|
||||||
|
yConstraint = toast.topAnchor.constraint(equalTo: parentSafeArea.topAnchor, constant: config.edgeSpacing)
|
||||||
|
case .bottom:
|
||||||
|
yConstraint = parentSafeArea.bottomAnchor.constraint(equalTo: toast.bottomAnchor, constant: config.edgeSpacing)
|
||||||
|
case .automatic:
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
toast.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: parentSafeArea.leadingAnchor, multiplier: 1),
|
||||||
|
parentSafeArea.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: toast.trailingAnchor, multiplier: 1),
|
||||||
|
parentSafeArea.centerXAnchor.constraint(equalTo: toast.centerXAnchor),
|
||||||
|
yConstraint,
|
||||||
|
])
|
||||||
|
|
||||||
|
if animated {
|
||||||
|
toast.animateAppearance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func effectiveEdge(edge: ToastConfiguration.Edge) -> ToastConfiguration.Edge {
|
||||||
|
guard case .automatic = edge else {
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return .bottom
|
||||||
|
} else {
|
||||||
|
return .top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate class WeakHolder<T: AnyObject> {
|
||||||
|
weak var object: T?
|
||||||
|
|
||||||
|
init(object: T) {
|
||||||
|
self.object = object
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue