// // 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 } var toastScrollView: UIScrollView? { get } } private var currentToastKey = "Tusker_currentToast" extension ToastableViewController { private(set) var currentToast: ToastView? { get { let holder = objc_getAssociatedObject(self, ¤tToastKey) as? WeakHolder 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 } var toastScrollView: UIScrollView? { view as? UIScrollView } 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() } if config.dismissOnScroll, let scrollView = toastScrollView { toast.setupDismissOnScroll(connectedTo: scrollView) } if let time = config.dismissAutomaticallyAfter { DispatchQueue.main.asyncAfter(deadline: .now() + time) { [weak toast] in guard let toast = toast, toast.shouldDismissAutomatically else { return } toast.dismissToast(animated: true) } } } 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 { weak var object: T? init(object: T) { self.object = object } }