I try to simply move the UIView
using animation. Here is my sample code.
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tap1(gesture:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
self.tempView.addGestureRecognizer(tapGesture)
self.tempView.frame = CGRect(x: self.tempView.frame.origin.x, y: 0, width: self.tempView.frame.width, height: self.tempView.frame.height)
UIView.animate(withDuration: 15, delay: 0.0, options: [.allowUserInteraction, .allowAnimatedContent], animations: {
self.tempView.frame = CGRect(x: self.tempView.frame.origin.x, y: UIScreen.main.bounds.height - 100, width: self.tempView.frame.width, height: self.tempView.frame.height)
}) { (_) in
}
Here I mention tap gesture method:
@objc func tap1(gesture: UITapGestureRecognizer) {
print("tap with object")
}
The problem is, when UIView
is animating and moves from one position to another position, at that time tap gesture is not working.
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.