Getting animation height during animation in Swift / iOS - ios

I am trying to get the current height of an animated UIView on my app.
I animate the UIView using:
function startFilling() {
UIView.animateWithDuration(1.5, delay: 0, options: CurveEaseIn, animations: { () -> Void in
self.filler.frame = CGRectMake(0, 0, self.frame.width, self.frame.height)
}) { (success) -> Void in
if success {
self.removeGestureRecognizer(self.tap)
self.delegate?.showSuccessLabel!()
} else if !success {
print("Not successful")
}
}
}
This animation is triggered using the .Begin state of a UILongPressGestureRecognizer like so:
if press.state == UIGestureRecognizerState.Began {
startFilling()
} else if press.state == UIGestureRecognizerState.Ended {
endFilling()
}
If the user stops pressing then the function endFilling is called and I need to get the current height of self.filler at the time they stop pressing.
The trouble is that the height always returns the expected end height in the animation (eg. self.frame.height), even if the press only lasted a portion of the duration time.
How can I get this height?

That's not really how core animation works. Once you set a animatable value in a UIView.animateWithDuration block that value is set, whether the animation is completed or not. Thus when you ask for self.filler.frame after you call startFilling it is equal to CGRectMake(0, 0, self.frame.width, self.frame.height) regardless of whether it's 0.5 seconds or 5.0 seconds after you call startFilling. If you want to get the current value of the frame for what is on the screen, you need to say self.filler.layer.presentationLayer.frame. This should be used as a readonly value. If you want to interrupt the animation and have it come to rest at this value, you should say something like:
UIView.animateWithDuration(1.5, delay: 0, options: BeginFromCurrentState, animations: {
self.filler.frame = self.layer.filler.presentationLayer.frame
})

In swift4
self.filler.layer.presentation().bounds

Related

move to completion instantly if no animations affected

I'm using
UIView.animated(withDuration:animations:completion:) function and there are sometimes that there is no animations affected in the animations block
For example:
Let's assume that I have a view, and it's frame.origin.y is already equals to 0.
Now the animation that I wan't to make is that:
UIView.animate(
withDuration: 1,
animations: {
self.view.frame.origin.y = 0
}
completion: { completed in
guard completed else { return }
// do something
}
)
The completion block called after 1 second instead of instantly.
How can I make that the completion block will called instantly if there are no animations affected in the animations block without any duration.
This is a something that you should handle yourself , the animations won't know that , you can make a compare like
if self.view.frame.origin.y != someValue {
// do animation
}
else {
// run some other code
}
Replace
withDuration: 1,
With
withDuration: 0.01,
(Or even less)

Pan Gesture completion handler getting called immediately

I'm animating a view to move as the user pans the screen. I have kept a threshold after which the view will animate to a default position.
The problem currently is that the completion handler of the animate method which resets the view to a position is called before the duration. The animation seems to be happening abruptly instead of over a duration of time.
// Pan gesture selector method
#objc func panAction(for panGesture: UIPanGestureRecognizer) {
switch panGesture.state {
case .began:
//Began code
case changed:
if (condition) {
print("IF")
//Change constraint constant of customView
animate(view: customView)
} else if (condition) {
print("ELSE IF")
//Change constraint constant of customView
animate(view: customView)
} else {
//Change constraint constant of customView
print("ELSE")
view.layoutIfNeeded()
}
case .ended:
//Ended code
default:
break
}
}
The animate method:
func animate(view: UIView) {
UIView.animate(withDuration: 3, delay: 0, options: .curveEaseOut, animations: {
view.layoutIfNeeded()
}, completion: { (finished) in
if finished {
flag = false
}
})
}
The flag is being set immediately rather than after 3 seconds.
The o/p i get while panning and crossing threshold.
ELSE
ELSE
ELSE
ELSE
IF
Edit: I am an idiot. I did not call layoutIfNeeded() on the superView.
Your gesture sends several events while the gesture is happening, and as you call your UIView.animate() code multiple times the new value supersedes the previous one.
Try adding the animation option .beginFromCurrentState:
UIView.animate(withDuration: 0.5, delay: 0, options: [.beginFromCurrentState,.allowAnimatedContent,.allowUserInteraction], animations: {
view.layoutIfNeeded()
}) { (completed) in
...
}
And expect your completed to be called multiple times with the completed == false as the gesture is progressing.
Edit: Your issue may also be related to calling layoutIfNeeded() on the wrong view, possibly try to call this on the viewController.view ?
I solved this. I was not calling layoutIfNeeded on the superview.

UISlider get Value while Anmation runs

I animate a UISlider with this function:
func animateSlider(){
slider.value = Float(min)
UIView.animate(withDuration: 2.0, animations: {
self.slider.setValue(Float(self.max), animated:true)
})
When I tap the screen I want to get the current value but it only returns the maximum value. What can I do to solve that problem?
When you schedule an animation Core Animation sets the model layer's properties immediately, and the creates a presentation layer which you may inspect. This presentation layer is what you actually see on screen, and is valid to query its properties such as its frame, bounds, position, etc.
You were asking for the model layer's value which is set immediately in your animation block, when instead what you want is what is displayed through the presentation layer.
You can get the currently displayed presentation value from the label when there is an in-progress animation by using this extension on UISlider:
extension UISlider {
var currentPresentationValue: Float {
guard let presentation = layer.presentation(),
let thumbSublayer = presentation.sublayers?.max(by: {
$0.frame.height < $1.frame.height
})
else { return self.value }
let bounds = self.bounds
let trackRect = self.trackRect(forBounds: bounds)
let minRect = self.thumbRect(forBounds: bounds, trackRect: trackRect, value: 0)
let maxRect = self.thumbRect(forBounds: bounds, trackRect: trackRect, value: 1)
let value = (thumbSublayer.frame.minX - minRect.minX) / (maxRect.minX - minRect.minX)
return Float(value)
}
}
And its usage:
func animateSlider() {
slider.value = 0
DispatchQueue.main.async {
UIView.animate(withDuration: 2, delay: 0, options: .curveLinear, animations: {
self.slider.setValue(1, animated: true)
}, completion: nil)
}
// quarter done:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
print(self.slider.currentPresentationValue)
// prints 0.272757, expected 0.25 with some tolerance
}
// half done:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print(self.slider.currentPresentationValue)
// prints 0.547876, expected 0.5 with some tolerance
}
// three quarters done:
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
print(self.slider.currentPresentationValue)
// prints 0.769981, expected 0.75 with some tolerance
}
}
I don't think that a slider's value property can be animated using UIView.animate(). However, if you read the documentation on UISlider, you'll find the function
func setValue(Float, animated: Bool)
Use that to animate your slider to a new position.
If you want to animate a change over a longer time period like 2 seconds, though, you'll probably have to set up a repeating timer or a CADisplayLink timer that changes the slider value by small increments each time it fires.
As for your statement "When I tap the screen I want to get the current value but it only returns the maximum" That is the way Cocoa animation works. You can't interrogate an in-flight animation and get a value that's the value of the property you are animating at that instant. (You could do that if you used a repeating timer to animate the slider though.)

old swift code is not working (animating a UIView)

i have found this code which is responsible for animating a UIView but unfortunately the code does not work and i can not figure the reason (maybe an older version of swift)
this is the code :
(this is helper function according to the creator)
func moveView(#view:UIView, toPoint destination:CGPoint, completion☹()->())?) {
//Always animate on main thread
dispatch_async(dispatch_get_main_queue(), { () Void in
//Use UIView animation API
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping:
0.6, initialSpringVelocity: 0.3, options:
UIViewAnimationOptions.AllowAnimatedContent, animations: { () ->
Void in
//do actual move
view.center = destination
}, completion: { (complete) -> Void in
//when animation completes, activate block if not nil
if complete {
if let c = completion {
c()
}
}
})
})
}
and this is the animation
//Create your face object (Just a UIImageView with a face as the image
var face = Face();
face.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
//find our trajectory points
var center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2);
var left = CGPointMake(center.x *-0.3, center.y)
var right = CGPointMake(center.x *2.2, center.y)
//place our view off screen
face.center = right
self.view.addSubview(face)
//move to center
moveView(view: face, toPoint: center) { () -> () in
//Do your Pop
face.pop()
// Move to left
moveView(view: face, toPoint: left, completion: { () -> () in
}
}
and i quote from the creator of the code
General Steps: Create a new face on the right edge of the screen. Make
the face visible. Move the face to the middle of the screen. Pop the
face Start the process with the next face. Move the first face to the
left as soon as the new face gets to the middle.
Actual slide animation Once again, we will do the following here: Move
view off screen on the right Move to center Pop Move to left
To get the repeating effect, just call this method on a timer
and a summary :
UIView’s animation API is very powerful. Both the pop and movement
animations use depend on this API. If you’re stuck with trying to
create an animation, UIView animation block is usually a good place to
start.
NOTE : im a beginner in IOS development if anyone can please explain the code for me
Indeed this moveView method had a few issues, one being it was written for Swift 1 (but there were also some typos, faulty characters and useless operations).
Here's the fixed version:
func moveView(view view:UIView, toPoint destination: CGPoint, afterAnim: ()->()) {
//Always animate on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Use UIView animation API
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping:
0.6, initialSpringVelocity: 0.3, options:
UIViewAnimationOptions.AllowAnimatedContent, animations: { () -> Void in
//do actual move
view.center = destination
}, completion: { (complete) -> Void in
//if and when animation completes, callback
if complete {
afterAnim()
}
})
})
}
You can use it like this now:
moveView(view: face, toPoint: center) {
//Do your Pop
face.pop()
// Move to left
moveView(view: face, toPoint: left) {
// Do stuff when the move is finished
}
}
Observe the differences between your version and mine to understand what was obsolete/wrong and how I fixed it. I'll help if you're stuck.

UIView.animateWithDuration completes too early when changing view with Tab Bar Controller

I am making a progress bar by increasing the with of a simple image:
let progressBar = createProgressBar(width: self.view.frame.width, height: 60.0)
let progressBarView = UIImageView(image: progressBar)
progressBarView.frame = CGRect(x: 0, y: 140, width: 0, height: 60)
UIView.animateWithDuration(60.0, delay: 0.0, options: [], animations: {
progressBarView.frame.size.width = self.backgroundView.frame.size.width
}, completion: {_ in
print("progress completed")
}
)
This works as expected, but I am having problems when changing views using a TabBarController. When I change view, I would like the progress bar to continue animating in the background, such that I can go back to this view to check on progress, but instead it does end immediately when I change views, and the completion block is called.
Why does this happen, and how to fix it?
When you tap another tabBar item, the viewController that is performing animation will be in a state of viewDidDisappear and the animation will be removed.
Actually, it is not recommended to perform any animation when the viewController is not presented in the front of the screen.
To continue the interrupted animation progress, you have to maintain the current states of the animation and restore them when the tabBar item switched back.
For example, you may hold some instance variables to keep the duration, progress, beginWidth and endWidth of the animation. And you can restore the animation in viewDidAppear:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// `self.duration` is the total duration of the animation. In your case, it's 60s.
// `self.progress` is the time that had been consumed. Its initial value is 0s.
// `self.beginWidth` is the inital width of self.progressBarView.
// `self.endWidth`, in your case, is `self.backgroundView.frame.size.width`.
if self.progress < self.duration {
UIView.animateWithDuration(self.duration - self.progress, delay: 0, options: [.CurveLinear], animations: {
self.progressBarView.frame.size.width = CGFloat(self.endWidth)
self.beginTime = NSDate() // `self.beginTime` is used to track the actual animation duration before it interrupted.
}, completion: { finished in
if (!finished) {
let now = NSDate()
self.progress += now.timeIntervalSinceDate(self.beginTime!)
self.progressBarView.frame.size.width = CGFloat(self.beginWidth + self.progress/self.duration * (self.endWidth - self.beginWidth))
} else {
print("progress completed")
}
}
)
}
}

Resources