diff --git a/README.md b/README.md index 84de04a..b41a412 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ The `text`, `default` and `custom` methods support custom configuration options. | `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` | | `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`| | `attachTo` | The view which the toast view will be attached to. | `UIView` | `nil` | @@ -97,6 +99,31 @@ let config = ToastConfiguration( let toast = toast.text("Safari pasted from Notes", config: config) ``` +### Custom entering/exiting animations +```swift +self.toast = Toast.text( + "Safari pasted from Noted", + config: .init( + direction: .bottom, + enteringAnimation: .fade(alphaValue: 0.5), + exitingAnimation: .slide(x: 0, y: 100)) + ).show() +``` +The above configuration will show a toast that will appear on screen with an animation of fade-in. And then when exiting will go down and disapear. + +```swift +self.toast = Toast.text( + "Safari pasted from Noted", + config: .init( + direction: .bottom, + enteringAnimation: .scale(scaleX: 0.6, scaleY: 0.6), + exitingAnimation: .default + ).show() +``` +The above configuration will show a toast that will appear on screen with scaling up animation from 0.6 to 1.0. And then when exiting will use our default animation (which is scaleAndSlide) + +For more on animation see the `Toast.AnimationType` enum. + ### Custom toast view Don't like the default Apple'ish style? No problem, it is also possible to use a custom toast view with the `custom` method. Firstly, create a class that confirms to the `ToastView` protocol: ```swift diff --git a/Sources/Toast/Toast.swift b/Sources/Toast/Toast.swift index 5c14a33..7566b32 100644 --- a/Sources/Toast/Toast.swift +++ b/Sources/Toast/Toast.swift @@ -13,6 +13,29 @@ public class Toast { case top, bottom } + /// Built-in animations for your toast + public enum AnimationType { + /// Use this type for fading in/out animations. + case slide(x: CGFloat, y: CGFloat) + + /// 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) + + /// Use this type for scaling and slide in/out animations. + case scaleAndSlide(scaleX: CGFloat, scaleY: CGFloat, x: CGFloat, y: CGFloat) + + /// Use this type for scaling in/out animations. + case scale(scaleX: CGFloat, scaleY: CGFloat) + + /// Use this type for giving your own affine transformation + case custom(transformation: CGAffineTransform) + + /// Currently the default animation if no explicit one specified. + case `default` + } + private var closeTimer: Timer? /// This is for pan gesture to close. @@ -35,15 +58,6 @@ public class Toast { private(set) var direction: Direction - private var initialTransform: CGAffineTransform { - switch self.direction { - case .top: - return CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -100) - case .bottom: - return CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: 100) - } - } - /// Creates a new Toast with the default Apple style layout with a title and an optional subtitle. /// - Parameters: /// - title: Attributed title which is displayed in the toast view @@ -137,8 +151,7 @@ public class Toast { self.config = config self.view = view self.direction = config.direction - - view.transform = initialTransform + if config.enablePanToClose { enablePanToClose() } @@ -161,8 +174,9 @@ public class Toast { delegate?.willShowToast(self) + config.enteringAnimation.apply(to: self.view) UIView.animate(withDuration: config.animationTime, delay: delay, options: [.curveEaseOut, .allowUserInteraction]) { - self.view.transform = .identity + self.config.enteringAnimation.undo(from: self.view) } completion: { [self] _ in delegate?.didShowToast(self) closeTimer = Timer.scheduledTimer(withTimeInterval: .init(config.displayTime), repeats: false) { [self] _ in @@ -183,7 +197,7 @@ public class Toast { delay: 0, options: [.curveEaseIn, .allowUserInteraction], animations: { - self.view.transform = self.initialTransform + self.config.exitingAnimation.apply(to: self.view) }, completion: { _ in self.view.removeFromSuperview() completion?() @@ -210,7 +224,7 @@ public class Toast { public extension Toast{ private func enablePanToClose() { - let pan = UIPanGestureRecognizer(target: self, action: #selector(toastOnPan)) + let pan = UIPanGestureRecognizer(target: self, action: #selector(toastOnPan(_:))) self.view.addGestureRecognizer(pan) } @@ -226,16 +240,24 @@ public extension Toast{ closeTimer?.invalidate() // prevent timer to fire close action while being touched case .changed: let delta = gesture.location(in: topVc.view).y - startShiftY - if delta <= 0{ - self.view.frame.origin.y = startY + delta + 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 + } } case .ended: - let threshold = initialTransform.ty + (startY - initialTransform.ty) * 2 / 3 + let threshold = 15.0 // if user drags more than threshold the toast will be dismissed + let ammountOfUserDragged = abs(startY - self.view.frame.origin.y) + let shouldDismissToast = ammountOfUserDragged > threshold - if self.view.frame.origin.y < threshold { + if shouldDismissToast { close() - }else{ - // move back to origin position + } else { UIView.animate(withDuration: config.animationTime, delay: 0, options: [.curveEaseOut, .allowUserInteraction]) { self.view.frame.origin.y = self.startY } completion: { [self] _ in @@ -246,6 +268,13 @@ public extension Toast{ } } } + + case .cancelled, .failed: + closeTimer = Timer.scheduledTimer(withTimeInterval: .init(config.displayTime), repeats: false) { [self] _ in + if config.autoHide { + close() + } + } default: break } @@ -261,3 +290,42 @@ public extension Toast{ close() } } + +fileprivate extension Toast.AnimationType { + /// Applies the effects to the ToastView. + func apply(to view: UIView) { + switch self { + case .slide(x: let x, y: let y): + view.transform = CGAffineTransform(translationX: x, y: y) + + case .fade(let value): + view.alpha = value + + case .scaleAndSlide(let scaleX, let scaleY, let x, let y): + view.transform = CGAffineTransform(scaleX: scaleX, y: scaleY).translatedBy(x: x, y: y) + + case .scale(let scaleX, let scaleY): + view.transform = CGAffineTransform(scaleX: scaleX, y: scaleY) + + case .custom(let transformation): + view.transform = transformation + + case .`default`: + break + } + } + + /// Undo the effects from the ToastView so that it never happened. + func undo(from view: UIView) { + switch self { + case .slide, .scaleAndSlide, .scale, .custom: + view.transform = .identity + + case .fade: + view.alpha = 1.0 + + case .`default`: + break + } + } +} diff --git a/Sources/Toast/ToastConfiguration.swift b/Sources/Toast/ToastConfiguration.swift index aab8605..d6203ed 100644 --- a/Sources/Toast/ToastConfiguration.swift +++ b/Sources/Toast/ToastConfiguration.swift @@ -14,16 +14,20 @@ public struct ToastConfiguration { public let enablePanToClose: Bool public let displayTime: TimeInterval public let animationTime: TimeInterval + public let enteringAnimation: Toast.AnimationType + public let exitingAnimation: Toast.AnimationType public let view: UIView? - /// 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. /// - 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, @@ -31,6 +35,8 @@ public struct ToastConfiguration { enablePanToClose: Bool = true, displayTime: TimeInterval = 4, animationTime: TimeInterval = 0.2, + enteringAnimation: Toast.AnimationType = .default, + exitingAnimation: Toast.AnimationType = .default, attachTo view: UIView? = nil ) { self.direction = direction @@ -38,6 +44,37 @@ public struct ToastConfiguration { self.enablePanToClose = enablePanToClose self.displayTime = displayTime self.animationTime = animationTime + self.enteringAnimation = enteringAnimation.isDefault ? Self.defaultEnteringAnimation(with: direction) : enteringAnimation + self.exitingAnimation = exitingAnimation.isDefault ? Self.defaultExitingAnimation(with: direction) : exitingAnimation self.view = view } } + +// MARK: Default animations +private extension ToastConfiguration { + private static func defaultEnteringAnimation(with direction: Toast.Direction) -> Toast.AnimationType { + switch direction { + case .top: + return .custom( + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -100) + ) + case .bottom: + return .custom( + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: 100) + ) + } + } + + private static func defaultExitingAnimation(with direction: Toast.Direction) -> Toast.AnimationType { + self.defaultEnteringAnimation(with: direction) + } +} + +fileprivate extension Toast.AnimationType { + var isDefault: Bool { + if case .default = self { + return true + } + return false + } +}