I have animateButtons() function of which I need to set completion handler only when the animation has finished. The problem is that in animateButtons() I can only set the completion handler just after imageView.startAnimating() so the whole timing gets compromised, as it's completion is used to launch other animations. I read in a another post with same exact issue, that I should set an NSTimer to set the completion handler , as I actually thought of doing, but I don't really know how to . I have set NSTimer to call setCompletion() function, but how would I set it to call animateButtons() completion handler ?
Can you point me in the right direction?
This is the function and the selector function:
static func animateButtons(completed: #escaping(Bool) ->(), imageView: UIImageView, images: [UIImage]) {
imageView.animationImages = images
imageView.animationDuration = 1.0 // check duration
imageView.animationRepeatCount = 1
imageView.startAnimating()
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(setCompletion), userInfo: nil, repeats: false)
// completed(true)
}
#objc func setCompletion() {
}
You can try to use inline callback
static func animateButtons(completed: #escaping(Bool) ->(), imageView: UIImageView, images: [UIImage]) {
imageView.animationImages = images
imageView.animationDuration = 1.0 // check duration
imageView.animationRepeatCount = 1
imageView.startAnimating()
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false, block: { (t) in
t.invalidate()
completed(true)
})
}
BTW a dispatch after can do the job also
static func animateButtons(completed: #escaping(Bool) ->(), imageView: UIImageView, images: [UIImage]) {
imageView.animationImages = images
imageView.animationDuration = 1.0 // check duration
imageView.animationRepeatCount = 1
imageView.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
completed(true)
}
}
Approach 1 (I prefer this one) (If you are animating Gif images)
Use a third-party library like Gifu https://github.com/kaishin/Gifu to calculate the runtime of your gif. It will work even if you change the gif image in the future.
static func animateButtons(completed: #escaping(Bool) ->(), imageView: UIImageView, images: [UIImage]) {
imageView.prepareForAnimation(withGIFNamed: "welcome", loopCount: 1) {
self.imageView.startAnimatingGIF()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2 + self.imageView.gifLoopDuration, execute: {
// Do your Task after animation completes
)
}
}
Approach 2 (Calculate the run Time of the animation and set a timer)
Related
I have the case, where the connect button is pressed, and continuously animated until the VPN has connected. Then I would like to stop the animation and load up a new set of images and animate that only once.
#IBOutlet weak var vpnButton: UIImageView!
var premiumImages: [UIImage] = []
var loadingImages: [UIImage] = []
#objc private func vpnStatusDidChange(_ notification: Notification) {
let nevpnconn = notification.object as! NEVPNConnection
let status = nevpnconn.status
switch status {
case NEVPNStatus.connecting:
self.animateImages(imageView: self.vpnButton, images: loadingImages, duration: 1.0, isInfinite: true)
self.vpnButton.image = UIImage(named: "Red-1")
}
The method that takes care of animation:
func animateImages(imageView: UIImageView, images: [UIImage], duration: Double, isInfinite: Bool=false) {
imageView.stopAnimating()
imageView.animationImages = images
imageView.animationDuration = duration
if !isInfinite {
imageView.animationRepeatCount = 1
} else {
imageView.animationRepeatCount = 0
}
imageView.startAnimating()
}
So whenever I call this, it would stop any existing animation in place and animate it with new images.
And once the VPN has connected:
case NEVPNStatus.connected:
self.vpnButton.highlightedImage = UIImage(named: "OrangePressed")
self.animateImages(imageView: self.vpnButton, images: premiumImages, duration: 0.25)
self.vpnButton.image = UIImage(named: "OrangePremium-3")
Only the loadingImages get properly animated. The second set premiumImages doesn't animate and only shows the last frame.
I suspect that stopAnimating() takes some time to accomplish, and ideally, I needed a completion handler here to know when to kick off the imageView.startAnimating().
First we generate a random number (for example from say 0 to 10):
If randomNumber = 0
Animate imageSet0 [here we create an ImageArray and an animate function – please see code below)
Else if randomNumber = 1
Animate imageSet1
Else if randomNumber = 2
Animate imageSet2
And so on…
Then we place a DispatchQueue timer that waits for the above animation to complete (time delay is equal to animationDuration), then we repeat the first step above and generate another random number and play another animation set:
DispatchQueue.main.asyncAfter(deadline: .now() + imageView.animationDuration) {
[Insert code that repeats the first step above and generates another random number to play another animation set]
}
In theory this random animation could play indefinitely until the user moves past this scene.
Here is my code thus far:
func createImageArray(total: Int, imagePrefix: String) -> [UIImage]{
var imageArray: [UIImage] = []
for imageCount in 0..<total {
let imageName = "\(imagePrefix)-\(imageCount).png"
let image = UIImage(named: imageName)!
imageArray.append(image)
}
return imageArray
}
func animate(imageView: UIImageView, images: [UIImage]){
imageView.animationImages = images
imageView.animationDuration = 1.0
imageView.animationRepeatCount = 1
imageView.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + imageView.animationDuration) {
[Create code that repeats the first step above and generates another random number to play another animation set]
}
}
You can use UIView.animate(withDuration: delay: options: animations:) api for that. Note delay parameter will let you start animation after certain delay.
So here is where I am at now. I still haven't been able to get it to work and I've tried a lot of variations. Here is my original code for a single animation.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
var images = [UIImage]()
var images2 = [UIImage]()
images += [#imageLiteral(resourceName: "circle1"), #imageLiteral(resourceName: "circle2"), #imageLiteral(resourceName: "circle3"), #imageLiteral(resourceName: "circle4")]
images2 += [#imageLiteral(resourceName: "circle4"), #imageLiteral(resourceName: "circle3"), #imageLiteral(resourceName: "circle2"), #imageLiteral(resourceName: "circle1")]
imageView.animationImages = images
imageView.animationDuration = 4.0
imageView.animationRepeatCount = 2
imageView.startAnimating()
}
}
I am able to animate the first images array with the properties I've set for duration and count. However, I just do not understand how I would write this to add a second set of images to animate immediately after. I know I need to use this somehow:
UIView
.animate(withDuration: 4.0,
animations: {
//animation 1
},
completion: { _ in
UIView.animate(withDuration: 6.0,
animations: {
//animation 2
})
})
Can someone show me how I would write this given my current images arrays using the same imageView object to display both animations? I want my first animation (images) to have a duration of 4 seconds and my second animation (images2) to have a duration of 6 second immediately following the first animation. I can't figure out what I need inside the animations parameter.
i think the error is that you mix here 2 things:
UIView.animate -> animate controls and properties
UIImageView.startAnimating -> start a loop of images in an UIImageView
but they don't do the same they are very independent. but UIView animation is normally for an other use case. only one thing is that they maybe have the same duration like your UIImageView animation, but you don't set the duration of your UIImageView. maybe when you set the animation duration of your image view to the duration of UIView animation then it is done on the same time range.
myImageView.animationDuration = 2;
and for the second loop
myImageView.animationDuration = 4;
Other Solutions
the thing is you need to know when an image loop ist completed. but there is no event for this (i did not found any)
there are some solutions on StackOverflow for this:
1 performSelector afterDelay
Set a timer to fire after the duration is done. for this you need also to add an animation duration to your image view:
myImageView.animationDuration = 0.7;
solution from here: how to do imageView.startAnimating() with completion in swift?:
When the button is pressed you can call a function with a delay
self.performSelector("afterAnimation", withObject: nil, afterDelay: imageView1.animationDuration)
Then stop the animation and add the last image of imageArray to the imageView in the afterAnimation function
func afterAnimation() {
imageView1.stopAnimating()
imageView1.image = imageArray.last
}
2 Timer
this is similar to performSelector afterDelay but with NSTimer.
you find a description here UIImageView startAnimating: How do you get it to stop on the last image in the series? :
1) Set the animation images property and start the animation (as in
your code in the question)
2) Schedule an NSTimer to go off in 'animationDuration' seconds time
3) When the timer goes off, [...]
add to point 3: then start the next animation
3 CATransaction
solution from here: Cocoa animationImages finish detection (you need to convert it to swift 3 syntax but it can be a starting point
Very old question, but I needed a fix now, this works for me:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
DLog(#"Animation did finish.");
}];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.window.bounds];
imageView.animationDuration = 0.3 * 4.0;
imageView.animationImages = #[[UIImage AHZImageNamed:#"Default2"],
[UIImage AHZImageNamed:#"Default3"],
[UIImage AHZImageNamed:#"Default4"],
[UIImage AHZImageNamed:#"Default5"]];
imageView.animationRepeatCount = 1;
[self.window addSubview:imageView];
[imageView startAnimating];
[CATransaction commit];
4 Offtopic: Manual do the Image Animation
thats a little offtopic, because here they do the image animation manually. for your use case you just change the logic which image from index is visible. count forward until last image, then backwards until first image. and then stop the loop. not nice solution but in this solution is added a image transition animation:
Solution from here: Adding Image Transition Animation in Swift
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
let images = [
UIImage(named: "brooklyn-bridge.jpg")!,
UIImage(named: "grand-central-terminal.jpg")!,
UIImage(named: "new-york-city.jpg"),
UIImage(named: "one-world-trade-center.jpg")!,
UIImage(named: "rain.jpg")!,
UIImage(named: "wall-street.jpg")!]
var index = 0
let animationDuration: NSTimeInterval = 0.25
let switchingInterval: NSTimeInterval = 3
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = images[index++]
animateImageView()
}
func animateImageView() {
CATransaction.begin()
CATransaction.setAnimationDuration(animationDuration)
CATransaction.setCompletionBlock {
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(self.switchingInterval * NSTimeInterval(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
self.animateImageView()
}
}
let transition = CATransition()
transition.type = kCATransitionFade
/*
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight
*/
imageView.layer.addAnimation(transition, forKey: kCATransition)
imageView.image = images[index]
CATransaction.commit()
index = index < images.count - 1 ? index + 1 : 0
}
}
Implement it as a custom image view would be better.
I know you said that this didn't work for you, but if you just want to execute animations right after another, I believe this really is the best option.
You should be able to do something like this:
let imageView = UIImageView()
UIView.animate(withDuration: 0.5, animations: {
//animation 1
}, completion: { (value: Bool) in
UIView.animate(withDuration: 1.0, animations: {
//animation 2
})
})
This does one animation directly after another, which a longer duration. Of course you still need to define what the animations are, but I hope this helps.
I'm not sure if this is the best way to do this, but I've found a solution to my problem. Please give feedback if there is a better way. I was able to accomplish multiple animations in a single UIImageView consecutively by using: self.perform(#selector(ViewController.afterAnimation), with: nil, afterDelay: 4.0)
This calls the afterAnimation function:
func afterAnimation() {
imageView.stopAnimating()
imageView.animationImages = images2
imageView.animationDuration = 6.0
imageView.animationRepeatCount = 1
imageView.startAnimating()
}
I needed animations to each last a specific amount of time and to be able to chain many of them together. It solves my problem.
I am using an NSTimer to let the user know the app is working. The progress bar is set up to last 3 seconds, but when running, it displays in a 'ticking' motion and it is not smooth like it should be. Is there anyway I can make it more smooth - I'm sure just a calculation error on my part.
Here is the code:
import UIKit
class LoadingScreen: UIViewController {
var time : Float = 0.0
var timer: NSTimer?
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
// Do stuff
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector:Selector("setProgress"), userInfo: nil, repeats: true)
} //close viewDidLoad
func setProgress() {
time += 0.1
progressView.progress = time / 3
if time >= 3 {
timer!.invalidate()
}
}
}
As per Apple iOS SDK docs you can achieve it with the use of next API:
func setProgress(_ progress: Float, animated animated: Bool)
It adjusts the current progress shown by the receiver, optionally animating the change.
Parameters:
progress - The new progress value.
animated - true if the change should be animated, false if the change should happen immediately.
More info on this:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIProgressView_Class/index.html#//apple_ref/occ/instm/UIProgressView/setProgress:animated:
So in your case you should do it like this:
func setProgress() {
time += 0.1
dispatch_async(dispatch_get_main_queue()) {
progressView.setProgress(time / 3, animated: true)
}
if time >= 3 {
timer!.invalidate()
}
}
Also please note that it is a good practice to perform UI updates on main thread, so I just dispatched progress update on main queue.
Hope it will help you.
I'm using the code below. How can I disable the button before the Finish Animation effect? In that case when I clicked the button before finish animating than animation image restarts and it is not very smooth.
#IBAction func btn_LeeStatue(sender: AnyObject) {
lee_statue_img.animationImages = [
UIImage(named: "lee_statueAni0001.png")!,
UIImage(named: "lee_statueAni0002.png")!,
UIImage(named: "lee_statueAni0003.png")!,
UIImage(named: "lee_statueAni0004.png")!,
UIImage(named: "lee_statueAni0005.png")!,
]
lee_statue_img.animationDuration = 3
lee_statue_img.animationRepeatCount = 1
lee_statue_img.startAnimating()
}
UIImageView image animation doesn't have a "completion handler". It is much more simple-minded than that.
If you want a completion handler, use some sort of animation that does have a completion handler.
(In your case, I don't see why you even need a completion handler. You know when this animation will end: X second from now. So just set an NSTimer to call you back then.)
so your code will be:
func showButton(timer : NSTimer) {
YourButtonOutlet.enabled = true
//this will enable you button again
}
#IBAction func btn_LeeStatue(sender: AnyObject) {
lee_statue_img.animationImages = [
UIImage(named: "lee_statueAni0001.png")!,
UIImage(named: "lee_statueAni0002.png")!,
UIImage(named: "lee_statueAni0003.png")!,
UIImage(named: "lee_statueAni0004.png")!,
UIImage(named: "lee_statueAni0005.png")!,
]
//Disable the button
YourButtonOutlet.enabled= false
lee_statue_img.animationDuration = 3
lee_statue_img.animationRepeatCount = 1
lee_statue_img.startAnimating()
//set the timer for X seconds (4s in this example)
let myTimer : NSTimer = NSTimer.scheduledTimerWithTimeInterval(4, target: self, selector: Selector("showButton:"), userInfo: nil, repeats: false)
}
and you can also use dispatch after,
just add this function->
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
and you can call it like this->(0.75 is the time that the app should wait before executing whats in the closure)
delay(0.75, closure: {
//write here the code to be executed after 0.75 second
//in your case it will be YourButtonOutlet.enabled = true
})
PS: you have to set YourButtonOutlet.enabled = false before the animation start
I think you want to disable a button from users touch animate it then enable it to a user's touch. To do that you toggle the userInteractionEnabled property.
let button: UIButton = UIButton()
button.userInteractionEnabled = false
button.animateMethod()
button.userInteractionEnabled = true