Tap Gesture on Animating Uiview Not Working

Tap gesture not detect on animating-moving object(UIView)

Although visually view changes position it does not do so. If you check it's frame while inside animation the frame will always be whatever is your end frame. So you can tap it wherever its end position will be. (This also assumes that you have enabled user interaction while animating which you did).

What you need for your case is to manually move your view with timer. It takes a bit more effort but can be easily doable. A timer should trigger with some nice FPS like 60 and each time it fires a frame should be updated to its interpolated position.

To do interpolation you can simply do it by component:

func interpolateRect(from: CGRect, to: CGRect, scale: CGFloat) -> CGRect {
return CGRect(x: from.minX + (to.minX - from.minX) * scale, y: from.minY + (to.minY - from.minY) * scale, width: from.width + (to.width - from.width) * scale, height: from.height + (to.height - from.height) * scale)
}

Scale naturally has to be between 0 and 1 for most cases.

Now you need to have a method to animate with timer like:

func animateFrame(to frame: CGRect, animationDuration duration: TimeInterval) {
self.animationStartFrame = tempView.frame // Assign new values
self.animationEndFrame = frame // Assign new values
self.animationStartTime = Date() // Assign new values

self.currentAnimationTimer.invalidate() // Remove current timer if any

self.currentAnimationTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
let timeElapsed = Date().timeIntervalSince(self.animationStartTime)
let scale = timeElapsed/duration
self.tempView.frame = self.interpolateRect(from: self.animationStartFrame, to: self.animationEndFrame, scale: CGFloat(max(0.0, min(scale, 1.0))))
if(scale >= 1.0) { // Animation ended. Remove timer
timer.invalidate()
self.currentAnimationTimer = nil
}
}
}

To be fair this code can still be reduced since we are using timer with block:

func animateFrame(to frame: CGRect, animationDuration duration: TimeInterval) {
let startFrame = tempView.frame
let endFrame = frame
let animationStartTime = Date()

self.currentAnimationTimer.invalidate() // Remove current timer if any

self.currentAnimationTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
let timeElapsed = Date().timeIntervalSince(animationStartTime)
let scale = timeElapsed/duration
self.tempView.frame = self.interpolateRect(from: startFrame, to: endFrame, scale: CGFloat(max(0.0, min(scale, 1.0))))
if(scale >= 1.0) { // Animation ended. Remove timer
timer.invalidate()
if(timer === self.currentAnimationTimer) {
self.currentAnimationTimer = nil // Remove reference only if this is the current timer
}
}
}
}

This way we don't need to store so many values in our class but keep the all inside the method.

It may be interesting to mention how to do easing. The trick is only to manipulate our scale. Consider something like this:

func easeInScaleFromLinearScale(_ scale: CGFloat, factor: CGFloat = 0.2) -> CGFloat {
return pow(scale, 1.0+factor)
}
func easeOutScaleFromLinearScale(_ scale: CGFloat, factor: CGFloat = 0.2) -> CGFloat {
return pow(scale, 1.0/(1.0+factor))
}

Now all we need to do is use it when interpolating:

self.tempView.frame = self.interpolateRect(from: startFrame, to: endFrame, scale: easeInScaleFromLinearScale(CGFloat(max(0.0, min(scale, 1.0)))))

Modifying the factor will change the effect. Higher the factor, higher effect. And using a factor at zero would mean a linear curve.

With this you can do pretty much any type of animation. The only rule is that your function must follow f(0) = 0 and f(1) = 1. This means it starts at starting position and end at ending position.

Some curves are a bit more tricky though. EaseInOut might seem simple but it is not. We would probably want to implement something like sin in range [-PI/2, PI/2]. Then balance this function with linear function. This is one of my implementations I found:

return (sin((inputScale-0.5) * .pi)+1.0)/2.0 * (magnitude) + (inputScale * (1.0-magnitude))

It helps if you play around with some "online graphing calculator" to find your equations and then convert results into your functions.

Tap Gesture on animating UIView not working

You will not be able to accomplish what you are after using a tapgesture for 1 huge reason. The tapgesture is associated with the frame of the label. The labels final frame is changed instantly when kicking off the animation and you are just watching a fake movie(animation). If you were able to touch (0,900) on the screen it would fire as normal while the animation is occuring. There is a way to do this a little bit different though. The best would be to uses touchesBegan. Here is an extension I just wrote to test my theory but could be adapted to fit your needs.For example you could use your actual subclass and access the label properties without the need for loops.

extension UIViewController{

public override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch = touches.first else{return}
let touchLocation = touch.locationInView(self.view)

for subs in self.view.subviews{
guard let ourLabel = subs as? UILabel else{return}
print(ourLabel.layer.presentationLayer())

if ourLabel.layer.presentationLayer()!.hitTest(touchLocation) != nil{
print("Touching")
UIView.animateWithDuration(0.4, animations: {
self.view.backgroundColor = UIColor.redColor()
}, completion: {
finished in
UIView.animateWithDuration(0.4, animations: {
self.view.backgroundColor = UIColor.whiteColor()
}, completion: {
finished in
})
})
}
}

}
}

You can see that it is testing the coordinates of the CALayer.presentationLayer()..That's what I was calling the movies. To be honest, I have still not wrapped my head completely around the presentation layer and how it works.

Why does my UITapGestureRecognizer does not get called when animating the UIView?

Looks like this occurs because of your animation. View is all the time in animation status and block tap gesture. U can try call it with delay instead of adding delay for your animation.

func showToastWithMessage() {
if !isShowingToast {
view.addSubview(toast)
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 1
self?.toast.frame.origin.y += 10
self?.isShowingToast = true
}, completion: { _ in
print("Completion")
})

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 0
self?.toast.frame.origin.y -= 10
}, completion: { [weak self] _ in
self?.isShowingToast = false
self?.toast.removeFromSuperview()
})
}

}
}

This way view going to animate status after 5 sec not with 5 sec delay.

UITapGestureRecognizer not work when I animate the UIImageView

You need to allow user interaction during animation.

UIViewAnimationOptions options = UIViewAnimationCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction;

How to detect a tap on an UIImageView while it is in the process of animation?

You CANNOT do what you want using UITapGestureRecognizer because it uses frame based detection and detects if a touch was inside your view by checking against its frame..

The problem with that, is that animations already set the view's final frame before the animation even begins.. then it animates a snapshot of your view into position before showing your real view again..

Therefore, if you were to tap the final position of your animation, you'd see your tap gesture get hit even though your view doesn't seem like it's there yet.. You can see that in the following image:

https://i.imgur.com/Wl9WRfV.png

(Left-Side is view-hierarchy inspector)..(Right-Side is the simulator animating).

To solve the tapping issue, you can try some sketchy code (but works):

import UIKit

protocol AnimationTouchDelegate {
func onViewTapped(view: UIView)
}

protocol AniTouchable {
var animationTouchDelegate: AnimationTouchDelegate? {
get
set
}
}

extension UIView : AniTouchable {
private struct Internal {
static var key: String = "AniTouchable"
}

private class func getAllSubviews<T: UIView>(view: UIView) -> [T] {
return view.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(view: subView) as [T]
if let view = subView as? T {
result.append(view)
}
return result
}
}

private func getAllSubviews<T: UIView>() -> [T] {
return UIView.getAllSubviews(view: self) as [T]
}

var animationTouchDelegate: AnimationTouchDelegate? {
get {
return objc_getAssociatedObject(self, &Internal.key) as? AnimationTouchDelegate
}

set {
objc_setAssociatedObject(self, &Internal.key, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}

override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let touchLocation = touch.location(in: self)

var didTouch: Bool = false
let views = self.getAllSubviews() as [UIView]
for view in views {
if view.layer.presentation()?.hitTest(touchLocation) != nil {
if let delegate = view.animationTouchDelegate {
didTouch = true
delegate.onViewTapped(view: view)
}
}
}

if !didTouch {
super.touchesBegan(touches, with: event)
}
}
}

class ViewController : UIViewController, AnimationTouchDelegate {

@IBOutlet weak var myImageView: UIImageView!

deinit {
self.myImageView.animationTouchDelegate = nil
}

override func viewDidLoad() {
super.viewDidLoad()

self.myImageView.isUserInteractionEnabled = true
self.myImageView.animationTouchDelegate = self
}

func onViewTapped(view: UIView) {
print("Works!")
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

myImageView.center.y += view.bounds.height
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

UIView.animate(withDuration: 5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
self.myImageView.center.y -= self.view.bounds.height
})
}
}

It works by overriding touchesBegan on the UIView and then checking to see if any of the touches landed inside that view.

A MUCH better approach would be to just do it in the UIViewController instead..

import UIKit

protocol AnimationTouchDelegate : class {
func onViewTapped(view: UIView)
}

extension UIView {
private class func getAllSubviews<T: UIView>(view: UIView) -> [T] {
return view.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(view: subView) as [T]
if let view = subView as? T {
result.append(view)
}
return result
}
}

func getAllSubviews<T: UIView>() -> [T] {
return UIView.getAllSubviews(view: self) as [T]
}
}

class ViewController : UIViewController, AnimationTouchDelegate {

@IBOutlet weak var myImageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()

self.myImageView.isUserInteractionEnabled = true
}

func onViewTapped(view: UIView) {
print("Works!")
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

myImageView.center.y += view.bounds.height
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

UIView.animate(withDuration: 5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
self.myImageView.center.y -= self.view.bounds.height
})
}

override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let touchLocation = touch.location(in: self.view)

var didTouch: Bool = false
for view in self.view.getAllSubviews() {
if view.isUserInteractionEnabled && !view.isHidden && view.alpha > 0.0 && view.layer.presentation()?.hitTest(touchLocation) != nil {

didTouch = true
self.onViewTapped(view: view)
}
}

if !didTouch {
super.touchesBegan(touches, with: event)
}
}
}


Related Topics



Leave a reply



Submit