Swift - the completion ends before the animation does - ios

I currently have the problem that the completion of the animation function ends before the animation itself does.
The array progressBar[] includes multiple UIProgressViews. When one is finished animating I want the next one to start animating and so on. But right now they all start at once.
How can I fix this?
#objc func updateProgress() {
if self.index < self.images.count {
progressBar[index].setProgress(0.01, animated: false)
group.enter()
DispatchQueue.main.async {
UIView.animate(withDuration: 5, delay: 0.0, options: .curveLinear, animations: {
self.progressBar[self.index].setProgress(1.0, animated: true)
}, completion: { (finished: Bool) in
if finished == true {
self.group.leave()
}
})
}
group.notify(queue: .main) {
self.index += 1
self.updateProgress()
}
}
}

The problem is that UIView.animate() can only be used on animatable properties, and progress is not an animatable property. "Animatable" here means "externally animatable by Core Animation." UIProgressView does its own internal animations, and that conflicts with external animations. This is UIProgressView being a bit over-smart, but we can work around it.
UIProgressView does use Core Animation, and so will fire CATransaction completion blocks. It does not, however, honor the duration of the current CATransaction, which I find confusing since it does honor the duration of the current UIView animation. I'm not actually certain how both of these are true (I would think that the UIView animation duration would be implemented on the transaction), but it seems to be the case.
Given that, the way to do what you're trying looks like this:
func updateProgress() {
if self.index < self.images.count {
progressBar[index].setProgress(0.01, animated: false)
CATransaction.begin()
CATransaction.setCompletionBlock {
self.index += 1
self.updateProgress()
}
UIView.animate(withDuration: 5, delay: 0, options: .curveLinear,
animations: {
self.progressBar[self.index].setProgress(1.0, animated: true)
})
CATransaction.commit()
}
}
I'm creating a nested transaction here (with begin/commit) just in case there is some other completion block created during this transaction. That's pretty unlikely, and the code "works" without calling begin/commit, but this way is a little safer than messing with the default transaction.

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)

Problems pausing UIViewPropertyAnimation

I am using UIViewPropertyAnimator to make small contentOffset animations.
To keep my view controller lean I have the animation code in the UIScrollView subclass which is to be animated. I originally did my animations in a UIView.animate block, but I noticed that when the view disappears (i.e. another view is pushed on top of the view) the animation jumps to the end, so I am trying to implement a pause in the animation. In addition I wanted to reduce the CPU load through unnecessary animation calls.
var animator: UIViewPropertyAnimator?
...
func showAnimation() {
...
if animator == nil {
animator = UIViewPropertyAnimator(duration: duration, curve: .linear, animations: { [unowned self] in
self.contentOffset = endPoint
})
animator?.addCompletion { [unowned self] (position) in
if position == .end {
self.afterAnimation()
print("completion called")
}
}
}
}
func pauseAnimation() {
if animator?.state == .active {
animator?.pauseAnimation()
}
print("paused animation")
}
The method afterAnimation() just determines if the showAnimation() is to be called again or not.
In my view controller I basically have the following:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myScrollView.showAnimation()
}
override func viewWillDisappear(_ animated: Bool) {
viewWillDisappear(animated)
myScrollView.pauseAnimation()
}
Now this works well as long as the user does not stay on another screen too long.
For example if the user calls up the settings screen (which gets pushed onto the
navigation stack) the pause method is called - just as expected. If the user stays
in that screen too long, then the animator's completion method is called, even though
the animation is not completed. Once the user dismisses the settings screen again the
animation is frozen and does not continue.
I also tried working with
animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: duration, delay: 0, options: .curveLinear, animations: { [unowned self] in
self.contentOffset = endPoint
print("starting animation")
}) { [unowned self] _ in
self.afterAnimation()
print("completion called")
}
but the results were the same. After a while the completion block gets called.
How can I best solve this issue? Thanks!
EDIT: Through different print statements I am slowly having the idea that even when the animator gets paused, the animation's internal counter continues and calls the completion block when the animation should be finished.

Swift Progress View animation makes the bar go further than 100%

When using animations for the ProgressView, it works completely fine if I let the animation run out but if I try to run this code again while the animation is still going, it will add 100% to the current bar and make it go through the bar. Images should explain the situation in a more logical sense.
Progress starts at 1.0 (100%):
Progress is half-way through:
The code was ran again, resulting in the progress going above 100%, although it uses the correct amount of time to finish.
Here's the code used:
self.progressView.setProgress(1.0, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
UIView.animate(withDuration: 10, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: { [unowned self] in
self.progressView.setProgress(0, animated: true)
})
}
Thanks in advance!
Some quick research...
Turns out UIProgressView needs a little special effort to stop the current animation. See if this does the job for you:
// stop any current animation
self.progressView.layer.sublayers?.forEach { $0.removeAllAnimations() }
// reset progressView to 100%
self.progressView.setProgress(1.0, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// set progressView to 0%, with animated set to false
self.progressView.setProgress(0.0, animated: false)
// 10-second animation changing from 100% to 0%
UIView.animate(withDuration: 10, delay: 0, options: [], animations: { [unowned self] in
self.progressView.layoutIfNeeded()
})
}
The setProgress(_:animated:) method already handles the animation for you. When the animated parameter is set to true, the progress change will be animated.
Try without the animation block :
self.progressView.setProgress(1.0, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.progressView.setProgress(0.0, animated: true)
}
Also make sure this is not an autolayout issue. Check your constraints on the progress view and make sure its width is properly constrained.

UISlider set value

I have a UISlider and I want to set its value from 1 to 10. The code I use is.
let slider = UISlider()
slider.value = 1.0
// This works I know that
slider.value = 10.0
What I want to do is animate the UISlider so that it takes 0.5s to change. I don't want it to be as jumpy more smooth.
My idea so far is.
let slider = UISlider()
slider.value = 1.0
// This works I know that
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseInOut, animation: { slider.value = 10.0 } completion: nil)
I am looking for the solution in Swift.
EDITED
After some discussion, I thought I'd clarify the differences between the two suggested solutions:
Using the built-in UISlider method .setValue(10.0, animated: true).
Encapsulating this method in a UIView.animateWithDuration.
Since the author is asking explicitly for a change that will take 0.5s---possibly triggered by another action---the second solution is to prefer.
As an example, consider that a button is connected to an action that sets the slider to its maximum value.
#IBOutlet weak var slider: UISlider!
#IBAction func buttonAction(sender: AnyObject) {
// Method 1: no animation in this context
slider.setValue(10.0, animated: true)
// Method 2: animates the transition, ok!
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseInOut, animations: {
self.slider.setValue(10.0, animated: true) },
completion: nil)
}
Running a simple single UIVIewController app with just the UISlider and UIButton objects present yields the following results.
Method 1: Instant slide (even though animated: true)
Method 2: Animates transition. Note that if we set animated: false in this context, the transition will be instantaneous.
the problem with #dfri's answer is that the blue Minimum Tracker is moving from 100% to the value, so in order to solve that, you need to change the method a little bit:
extension UISlider
{
///EZSE: Slider moving to value with animation duration
public func setValue(value: Float, duration: Double) {
UIView.animateWithDuration(duration, animations: { () -> Void in
self.setValue(self.value, animated: true)
}) { (bol) -> Void in
UIView.animateWithDuration(duration, animations: { () -> Void in
self.setValue(value, animated: true)
}, completion: nil)
}
}
}

UIScrollView Paging with Animation Completion Handler

I have a UIScrollview with buttons that I use for paging right and left:
#IBAction func leftPressed(sender: AnyObject) {
self.scrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
}
I'd like to perform an action after the scrollview has finished the paging animation. Something like:
#IBAction func leftPressed(sender: AnyObject) {
self.scrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
secondFunction()
}
The above code doesn't work because the second function runs before the scrollview is finished animating the offset. My initial reaction was to use a completion handler but I'm not sure how to apply one to the setContentOffset function. I've tried:
func animatePaging(completion: () -> Void) {
self.mainScrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
completion()
}
with the call
animatePaging(completion: self.secondFunction())
But I get the error "Cannot invoke 'animatePaging' with an argument list of type '(completion())'. Any thoughts?
The problem is that you need a completion handler for the scrolling animation itself. But setContentOffset(_:animated:) does not have a completion handler.
One solution would be that you animate the scrolling yourself using UIView's static function animateWithDuration(_:animations:completion:). That function has a completion handler that you can use:
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.scrollView.contentOffset = CGPointMake(0, 0)
}) { (finished) -> Void in
self.secondFunction()
}
Update from joern answer - Swift 4.2
UIView.animate(withDuration: 0.5, animations: { [unowned self] in
self.scrollView.contentOffset = .zero
}) { [unowned self] _ in
self.secondFunction()
}

Resources