diff --git a/README.md b/README.md index 28f843c..4233588 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A Swift Toast view - iOS 14 style - built with UIKit. 🍞 You can use The Swift Package Manager to install Toast-Swift by adding the description to your Package.swift file: ```swift dependencies: [ - .package(url: "https://github.com/BastiaanJansen/toast-swift", from: "1.5.0") + .package(url: "https://github.com/BastiaanJansen/toast-swift", from: "1.6.0") ] ``` @@ -78,9 +78,7 @@ The `text`, `default` and `custom` methods support custom configuration options. | Name | Description | Type | Default | |-----------------|-----------------------------------------------------------------------------------------------------|----------------|---------| | `direction` | Where the toast will be shown. | `.bottom` or `.up` | `.up` | -| `autoHide` | When set to true, the toast will automatically close itself after display time has elapsed. | `Bool` | `true` | -| `enablePanToClose` | When set to true, the toast will be able to close by swiping up. | `Bool` | `true` | -| `displayTime` | The duration the toast will be displayed before it will close when autoHide set to true in seconds. | `TimeInterval` | `4` | +| `dismissBy` | Choose when the toast dismisses. | `Dismissable` | [`.time`, `.swipe`] | | `animationTime` | Duration of the show and close animation in seconds. | `TimeInterval` | `0.2` | | `enteringAnimation` | The type of animation that will be used when toast is showing | `.slide`, `.fade`, `.scaleAndSlide`, `.scale` and `.custom` | `.default`| | `exitingAnimation` | The type of animation that will be used when toast is exiting | `.slide`, `.fade`, `.scaleAndSlide`, `.scale` and `.custom` | `.default`| @@ -90,9 +88,7 @@ The `text`, `default` and `custom` methods support custom configuration options. ```swift let config = ToastConfiguration( direction: .top, - autoHide: true, - enablePanToClose: true, - displayTime: 5, + dismissBy: [.time(time: 4.0), .swipe(direction: .natural), .longPress], animationTime: 0.2 ) diff --git a/Sources/Toast/AnimationType.swift b/Sources/Toast/AnimationType.swift index 776fe55..d98bf8d 100644 --- a/Sources/Toast/AnimationType.swift +++ b/Sources/Toast/AnimationType.swift @@ -17,7 +17,7 @@ extension Toast { /// Use this type for fading in/out animations. /// /// alphaValue must be greater or equal to 0 and less or equal to 1. - case fade(alphaValue: CGFloat) + case fade(alpha: CGFloat) /// Use this type for scaling and slide in/out animations. case scaleAndSlide(scaleX: CGFloat, scaleY: CGFloat, x: CGFloat, y: CGFloat) diff --git a/Sources/Toast/Direction.swift b/Sources/Toast/Direction.swift index 27991c5..a9ced24 100644 --- a/Sources/Toast/Direction.swift +++ b/Sources/Toast/Direction.swift @@ -14,4 +14,26 @@ extension Toast { case top, bottom } + public enum DismissSwipeDirection: Equatable { + case toTop, + toBottom, + natural + + func shouldApply(_ delta: CGFloat, direction: Direction) -> Bool { + switch self { + case .toTop: + return delta <= 0 + case .toBottom: + return delta >= 0 + case .natural: + switch direction { + case .top: + return delta <= 0 + case .bottom: + return delta >= 0 + } + } + } + } + } diff --git a/Sources/Toast/Queue/ToastQueue.swift b/Sources/Toast/Queue/ToastQueue.swift index 43a59b1..0999071 100644 --- a/Sources/Toast/Queue/ToastQueue.swift +++ b/Sources/Toast/Queue/ToastQueue.swift @@ -38,21 +38,21 @@ public class ToastQueue { } public func show() -> Void { - if (queue.isEmpty) { - return - } - show(index: 0) } - private func show(index: Int) -> Void { + private func show(index: Int, after: Double = 0.0) -> Void { + if queue.isEmpty { + return + } + let toast: Toast = queue.remove(at: index) let delegate = QueuedToastDelegate(queue: self) - multicast.invoke { $0.willShowAnyToast(toast) } + multicast.invoke { $0.willShowAnyToast(toast, queuedToasts: queue) } toast.addDelegate(delegate: delegate) - toast.show() + toast.show(after: after) } @@ -65,8 +65,8 @@ public class ToastQueue { } public func didCloseToast(_ toast: Toast) { - queue.multicast.invoke { $0.didShowAnyToast(toast) } - queue.show() + queue.multicast.invoke { $0.didShowAnyToast(toast, queuedToasts: queue.queue) } + queue.show(index: 0, after: 0.5) } } diff --git a/Sources/Toast/Queue/ToastQueueDelegate.swift b/Sources/Toast/Queue/ToastQueueDelegate.swift index 4d30f03..d49d4bb 100644 --- a/Sources/Toast/Queue/ToastQueueDelegate.swift +++ b/Sources/Toast/Queue/ToastQueueDelegate.swift @@ -9,16 +9,16 @@ import Foundation public protocol ToastQueueDelegate: AnyObject { - func willShowAnyToast(_ toast: Toast) -> Void + func willShowAnyToast(_ toast: Toast, queuedToasts: [Toast]) -> Void - func didShowAnyToast(_ toast: Toast) -> Void + func didShowAnyToast(_ toast: Toast, queuedToasts: [Toast]) -> Void } extension ToastQueueDelegate { - public func willShowAnyToast(toast: Toast) {} + public func willShowAnyToast(toast: Toast, queuedToasts: [Toast]) {} - public func didShowAnyToast(toast: Toast) {} + public func didShowAnyToast(toast: Toast, queuedToasts: [Toast]) {} } diff --git a/Sources/Toast/Toast.swift b/Sources/Toast/Toast.swift index 6aa010c..51de3ac 100644 --- a/Sources/Toast/Toast.swift +++ b/Sources/Toast/Toast.swift @@ -27,9 +27,7 @@ public class Toast { private var multicast = MulticastDelegate() - private let config: ToastConfiguration - - private(set) var direction: Direction + private(set) var config: ToastConfiguration /// Creates a new Toast with the default Apple style layout with a title and an optional subtitle. /// - Parameters: @@ -129,10 +127,18 @@ public class Toast { public required init(view: ToastView, config: ToastConfiguration) { self.config = config self.view = view - self.direction = config.direction - - if config.enablePanToClose { - enablePanToClose() + + for dismissable in config.dismissables { + switch dismissable { + case .tap: + enableTapToClose() + case .longPress: + enableLongPressToClose() + case .swipe: + enablePanToClose() + default: + break + } } } @@ -150,7 +156,7 @@ public class Toast { /// Show the toast /// - Parameter delay: Time after which the toast is shown public func show(after delay: TimeInterval = 0) { - config.view?.addSubview(view) ?? topController()?.view.addSubview(view) + config.view?.addSubview(view) ?? ToastHelper.topController()?.view.addSubview(view) view.createView(for: self) multicast.invoke { $0.willShowToast(self) } @@ -160,11 +166,8 @@ public class Toast { self.config.enteringAnimation.undo(from: self.view) } completion: { [self] _ in multicast.invoke { $0.didShowToast(self) } - closeTimer = Timer.scheduledTimer(withTimeInterval: .init(config.displayTime), repeats: false) { [self] _ in - if config.autoHide { - close() - } - } + + configureCloseTimer() } } @@ -190,36 +193,6 @@ public class Toast { multicast.add(delegate) } - private func topController() -> UIViewController? { - if var topController = keyWindow()?.rootViewController { - while let presentedViewController = topController.presentedViewController { - topController = presentedViewController - } - return topController - } - return nil - } - - private func keyWindow() -> UIWindow? { - if #available(iOS 13.0, *) { - for scene in UIApplication.shared.connectedScenes { - guard let windowScene = scene as? UIWindowScene else { - continue - } - if windowScene.windows.isEmpty { - continue - } - guard let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { - continue - } - return window - } - return nil - } else { - return UIApplication.shared.windows.first(where: { $0.isKeyWindow }) - } - } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -232,27 +205,28 @@ public extension Toast { } @objc private func toastOnPan(_ gesture: UIPanGestureRecognizer) { - guard let topVc = topController() else { + guard let topVc = ToastHelper.topController() else { return } - switch gesture.state{ + switch gesture.state { case .began: startY = self.view.frame.origin.y startShiftY = gesture.location(in: topVc.view).y closeTimer?.invalidate() case .changed: let delta = gesture.location(in: topVc.view).y - startShiftY - switch direction { - case .top: - if delta <= 0 { - self.view.frame.origin.y = startY + delta - } - case .bottom: - if delta >= 0 { - self.view.frame.origin.y = startY + delta + + for dismissable in config.dismissables { + if case .swipe(let dismissSwipeDirection) = dismissable { + let shouldApply = dismissSwipeDirection.shouldApply(delta, direction: config.direction) + + if shouldApply { + self.view.frame.origin.y = startY + delta + } } } + case .ended: let threshold = 15.0 // if user drags more than threshold the toast will be dismissed let ammountOfUserDragged = abs(startY - self.view.frame.origin.y) @@ -264,20 +238,12 @@ public extension Toast { UIView.animate(withDuration: config.animationTime, delay: 0, options: [.curveEaseOut, .allowUserInteraction]) { self.view.frame.origin.y = self.startY } completion: { [self] _ in - closeTimer = Timer.scheduledTimer(withTimeInterval: .init(config.displayTime), repeats: false) { [self] _ in - if config.autoHide { - close() - } - } + configureCloseTimer() } } case .cancelled, .failed: - closeTimer = Timer.scheduledTimer(withTimeInterval: .init(config.displayTime), repeats: false) { [self] _ in - if config.autoHide { - close() - } - } + configureCloseTimer() default: break } @@ -288,8 +254,32 @@ public extension Toast { self.view.addGestureRecognizer(tap) } + func enableLongPressToClose() { + let tap = UILongPressGestureRecognizer(target: self, action: #selector(toastOnTap)) + self.view.addGestureRecognizer(tap) + } + @objc func toastOnTap(_ gesture: UITapGestureRecognizer) { closeTimer?.invalidate() close() } + + private func configureCloseTimer() { + for dismissable in config.dismissables { + if case .time(let displayTime) = dismissable { + closeTimer = Timer.scheduledTimer(withTimeInterval: .init(displayTime), repeats: false) { [self] _ in + close() + } + } + } + } +} + +extension Toast { + public enum Dismissable: Equatable { + case tap, + longPress, + time(time: TimeInterval), + swipe(direction: DismissSwipeDirection) + } } diff --git a/Sources/Toast/ToastConfiguration.swift b/Sources/Toast/ToastConfiguration.swift index d6203ed..83d3f32 100644 --- a/Sources/Toast/ToastConfiguration.swift +++ b/Sources/Toast/ToastConfiguration.swift @@ -10,9 +10,7 @@ import UIKit public struct ToastConfiguration { public let direction: Toast.Direction - public let autoHide: Bool - public let enablePanToClose: Bool - public let displayTime: TimeInterval + public let dismissables: [Toast.Dismissable] public let animationTime: TimeInterval public let enteringAnimation: Toast.AnimationType public let exitingAnimation: Toast.AnimationType @@ -22,27 +20,21 @@ public struct ToastConfiguration { /// Creates a new Toast configuration object. /// - Parameters: /// - direction: The position the toast will be displayed. - /// - autoHide: When set to true, the toast will automatically close itself after display time has elapsed. - /// - enablePanToClose: When set to true, the toast will be able to close by swiping up. - /// - displayTime: The duration the toast will be displayed before it will close when autoHide set to true. + /// - dismissBy: Choose when the toast dismisses. /// - animationTime:Duration of the animation /// - enteringAnimation: The entering animation of the toast. /// - exitingAnimation: The exiting animation of the toast. /// - attachTo: The view on which the toast view will be attached. public init( direction: Toast.Direction = .top, - autoHide: Bool = true, - enablePanToClose: Bool = true, - displayTime: TimeInterval = 4, + dismissBy: [Toast.Dismissable] = [.time(time: 4.0), .swipe(direction: .natural)], animationTime: TimeInterval = 0.2, enteringAnimation: Toast.AnimationType = .default, exitingAnimation: Toast.AnimationType = .default, attachTo view: UIView? = nil ) { self.direction = direction - self.autoHide = autoHide - self.enablePanToClose = enablePanToClose - self.displayTime = displayTime + self.dismissables = dismissBy self.animationTime = animationTime self.enteringAnimation = enteringAnimation.isDefault ? Self.defaultEnteringAnimation(with: direction) : enteringAnimation self.exitingAnimation = exitingAnimation.isDefault ? Self.defaultExitingAnimation(with: direction) : exitingAnimation diff --git a/Sources/Toast/ToastHelper.swift b/Sources/Toast/ToastHelper.swift new file mode 100644 index 0000000..869d425 --- /dev/null +++ b/Sources/Toast/ToastHelper.swift @@ -0,0 +1,43 @@ +// +// File.swift +// +// +// Created by Bas Jansen on 16/09/2023. +// + +import Foundation +import UIKit + +class ToastHelper { + + public static func topController() -> UIViewController? { + if var topController = keyWindow()?.rootViewController { + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + return topController + } + return nil + } + + private static func keyWindow() -> UIWindow? { + if #available(iOS 13.0, *) { + for scene in UIApplication.shared.connectedScenes { + guard let windowScene = scene as? UIWindowScene else { + continue + } + if windowScene.windows.isEmpty { + continue + } + guard let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { + continue + } + return window + } + return nil + } else { + return UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + } + } + +} diff --git a/Sources/Toast/ToastViewConfiguration.swift b/Sources/Toast/ToastViewConfiguration.swift index f217038..cc0f1d7 100644 --- a/Sources/Toast/ToastViewConfiguration.swift +++ b/Sources/Toast/ToastViewConfiguration.swift @@ -15,15 +15,22 @@ public struct ToastViewConfiguration { public let darkBackgroundColor: UIColor public let lightBackgroundColor: UIColor + public let titleNumberOfLines: Int + public let subtitleNumberOfLines: Int + public init( minHeight: CGFloat = 58, minWidth: CGFloat = 150, darkBackgroundColor: UIColor = UIColor(red: 0.13, green: 0.13, blue: 0.13, alpha: 1.00), - lightBackgroundColor: UIColor = UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 1.00) + lightBackgroundColor: UIColor = UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 1.00), + titleNumberOfLines: Int = 1, + subtitleNumberOfLines: Int = 1 ) { self.minHeight = minHeight self.minWidth = minWidth self.darkBackgroundColor = darkBackgroundColor self.lightBackgroundColor = lightBackgroundColor + self.titleNumberOfLines = titleNumberOfLines + self.subtitleNumberOfLines = subtitleNumberOfLines } } diff --git a/Sources/Toast/ToastViews/AppleToastView/AppleToastView.swift b/Sources/Toast/ToastViews/AppleToastView/AppleToastView.swift index a4eddae..a938f78 100644 --- a/Sources/Toast/ToastViews/AppleToastView/AppleToastView.swift +++ b/Sources/Toast/ToastViews/AppleToastView/AppleToastView.swift @@ -44,7 +44,7 @@ public class AppleToastView : UIView, ToastView { centerXAnchor.constraint(equalTo: superview.centerXAnchor) ]) - switch toast.direction { + switch toast.config.direction { case .bottom: bottomAnchor.constraint(equalTo: superview.layoutMarginsGuide.bottomAnchor, constant: 0).isActive = true