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

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.

Related

UIView.transition with delay

I have simultaneous animations going on, and I would like to transition the VC in the middle (it will fade so it will see some of the other animations). However, I can't find documentation how to delay a transition similar to UIView.animateWithDuration.
I want to achieve this...
UIView.transition(withDuration: 0.5, delay: 0.1, options: .transitionCrossDissolve, animations: {})
I can manually add a delay like so.. but was wondering if there's a more elegant way.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {}
Unfortunately no. Using a bunch of UIView transitions/animations can get a little hairy.
In the past, I've resorted to setting up an NSTimer that fires 1/30 seconds and I ended up managing all the start times on my own.
What you can do is series out the animations using the closure.
UIView.animate(withDuration: 1, animations: {
}, completion: {Do UIView transition here})
It seems no way except surrounding with Timer:
Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { _ in
UIView.transition(with: self.myImageView, duration: 0.75,
options: [.transitionCrossDissolve],
animations: {
self.myImageView.image = newImage
})
})

Swift - the completion ends before the animation does

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.

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)
}
}
}

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")
}
}
)
}
}

Swift UIView animateWithDuration completion closure called immediately

I'm expecting the completion closure on this UIView animation to be called after the specified duration, however it appears to be firing immediately...
UIView.animateWithDuration(
Double(0.2),
animations: {
self.frame = CGRectMake(0, -self.bounds.height, self.bounds.width, self.bounds.height)
},
completion: { finished in
if(finished) {
self.removeFromSuperview()
}
}
)
Has anyone else experienced this? I've read that others had more success using the center rather than the frame to move the view, however I had the same problems with this method too.
For anyone else that is having a problem with this, if anything is interrupting the animation, the completion closure is immediately called. In my case, this was due to a slight overlap with a modal transition of the view controller that the custom segue was unwinding from. Using the delay portion of UIView.animateWithDuration(0.3, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations:{} had no effect for me. I ended up using GCD to delay animation a fraction of a second.
// To avoid overlapping with the modal transiton
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.2 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
// Animate the transition
UIView.animateWithDuration(0.3, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
// Animations
}, completion: { finished in
// remove the views
if finished {
blurView.removeFromSuperview()
snapshot.removeFromSuperview()
}
})
})
I resolved this in the end by moving the animation from hitTest() and into touchesBegan() in the UIView

Resources