Skip to content

Commit

Permalink
Merge pull request #25 from Nikoloutsos/feature/add-option-for-animation
Browse files Browse the repository at this point in the history
Add ability to choose entering/exiting animations and fix on pan gesture
  • Loading branch information
BastiaanJansen authored Jan 7, 2023
2 parents cb5d5aa + e5ce3aa commit deafc3b
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 21 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |


Expand All @@ -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
Expand Down
108 changes: 88 additions & 20 deletions Sources/Toast/Toast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -137,8 +151,7 @@ public class Toast {
self.config = config
self.view = view
self.direction = config.direction

view.transform = initialTransform

if config.enablePanToClose {
enablePanToClose()
}
Expand All @@ -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
Expand All @@ -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?()
Expand All @@ -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)
}

Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
}
}
39 changes: 38 additions & 1 deletion Sources/Toast/ToastConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,67 @@ 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,
autoHide: Bool = true,
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
self.autoHide = autoHide
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
}
}

0 comments on commit deafc3b

Please sign in to comment.