// // ToastableViewController.swift // ToastableViewController // // Created by Shadowfacts on 8/14/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit @MainActor protocol ToastableViewController: UIViewController { var toastParentView: UIView { get } var toastScrollView: UIScrollView? { get } } private let currentToastKey = UnsafeMutableRawPointer.allocate(byteCount: 0, alignment: 0) extension ToastableViewController { private(set) var currentToast: ToastView? { get { let holder = objc_getAssociatedObject(self, currentToastKey) as? WeakHolder return holder?.object } set { if let newValue = newValue { objc_setAssociatedObject(self, currentToastKey, WeakHolder(newValue), .OBJC_ASSOCIATION_RETAIN) } else { objc_setAssociatedObject(self, currentToastKey, 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 } } }