Repeating and reversing an animation multiple times using UIViewPropertyAnimator - ios

I am trying to have my animation ease the screen from black to white to black again and repeat that a certain amount of times. Currently with the code I have the animation eases from black to white then jumps back to black. Is there anyway to run an animation in reverse or add an animation that runs after the first animation is completed?
override func viewDidAppear(_ animated: Bool) {
let viewColorAnimator: UIViewPropertyAnimator = UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 4.0,
delay: 0.0,
options: [.curveEaseInOut],
animations: {
UIView.setAnimationRepeatCount(3);
self.lightView.backgroundColor = .white
})
viewColorAnimator.startAnimation()
}
I tried adding this block of code to the project but the outcome was the same:
viewColorAnimator.addCompletion {_ in
let secondAnimator = UIViewPropertyAnimator(duration: 4.0, curve: .linear) {
self.lightView.backgroundColor = .black
}
secondAnimator.startAnimation()
}
EDIT: I've found out it is because of the setAnimationRepeatCount because the last of the iterations works properly. How do I run the animation multiple times without the repeat count?

Documentation says that the options related to direction are ignored. you can check the image attached here.
To achieve reverse animation:
Create an animator property.
private var animator: UIViewPropertyAnimator = {
let propertyAnimator = UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 4.0,
delay: 0.0,
options: [.curveEaseInOut, .autoreverse],
animations: {
UIView.setAnimationRepeatCount(3)
UIView.setAnimationRepeatAutoreverses(true)
self.lightView.backgroundColor = .white
})
return propertyAnimator
}()
P.S. we need to mention the .autoreverse in the options. Otherwise UIView.setAnimationRepeatAutoreverses(true) won't work.

There's an easy way to run animations. And for this method: if you want the animation to repeat after completing, you can add the .autoreverse, and the .repeat option. Like this:
UIView.animate(withDuration: TimeInterval(randomTime), delay: 0, options: [.repeat,.autoreverse], animations: {
//Animation code here
}, completion: {(bool)
//Do something after completion
})
You can put the codes you want to execute after the animation in the completion block.
And, you can use a Timer as a way to run the animation for certain times.

Xcode 9.4.1 (Swift 4.1.2) and Xcode 10 beta 3 (Swift 4.2):
Here's the way to do it using UIViewPropertyAnimator -- basically, we just add .repeat and .autoreverse to the options. You were 99% of the way there:
override func viewDidAppear(_ animated: Bool) {
let viewColorAnimator: UIViewPropertyAnimator = UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 4.0,
delay: 0.0,
options: [.curveEaseInOut, .repeat, .autoreverse],
animations: {
UIView.setAnimationRepeatCount(3)
self.lightView.backgroundColor = .white
})
viewColorAnimator.startAnimation()
}

Related

For `UIView.animate`, is it possible to *chain* Swift Trailing Closures?

In Swift, I would like to have several calls to UIView.animate run in series. That is, when one animation finishes, then I would like another animation to continue after, and so on.
The call to UIView.animate has a Trailing Closure which I am currently using to make a second call to UIView.animate to occur.
The Problem is: I want to do N separate animations
From the Apple Documentation for UIView.animate
completion
A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.
Ideally, I would like to iterate over an array of animation durations and use in the calls to animate()
For example,
Goal
Iterate over an array and apply those parameters for each animation
let duration = [3.0, 5.0, 10.0]
let alpha = [0.1, 0.5, 0.66]
Xcode Version 11.4.1 (11E503a)
What I've tried
Use a map to iterate, and 🤞 hope that it works
Issue is that that only final animation occurs. So there is nothing in series
Research Q: Is it possible that I need to set a boolean in UIView that says animation occur in series?
let redBox = UIView()
redBox.backgroundColor = UIColor.red
self.view.addSubview(redBox)
let iterateToAnimate = duration.enumerated().map { (index, element) -> Double in
print(index, element, duration[index])
UIView.animate(withDuration: duration[index], // set duration from the array
animations: { () in
redBox.alpha = alpha[index]
}, completion:{(Bool) in
print("red box has faded out")
})
}
How to do one animation
let redBox = UIView()
redBox.backgroundColor = UIColor.red
self.view.addSubview(redBox)
// One iteration of Animation
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0
}, completion:{(Bool) in
print("red box has faded out")
})
Ugly way to do chain two animate iterations (wish to avoid)
// two iterations of Animation, using a trailing closure
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0
}, completion:{(Bool) in
print("red box has faded out")
}) { _ in // after first animation finishes, call another in a trailing closure
UIView.animate(withDuration: 1,
animations: { () in
redBox.alpha = 0.75
}, completion:{(Bool) in
print("red box has faded out")
}) // 2nd animation
}
If the parameters are few and are known at compile time, the simplest way to construct chained animations is as the frames of a keyframe animation.
If the parameters are not known at compile time (e.g. your durations are going to arrive at runtime) or there are many of them and writing out the keyframe animation is too much trouble, then just put a single animation and its completion handler into a function and recurse. Simple example (adapt to your own purposes):
var duration = [3.0, 5.0, 10.0]
var alpha = [0.1, 0.5, 0.66] as [CGFloat]
func doTheAnimation() {
if duration.count > 0 {
let dur = duration.removeFirst()
let alp = alpha.removeFirst()
UIView.animate(withDuration: dur, animations: {
self.yellowView.alpha = alp
}, completion: {_ in self.doTheAnimation()})
}
}
you can use UIView.animateKeyframes
UIView.animateKeyframes(withDuration: 18.0, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 3.0/15.0, relativeDuration: 3.0/15.0, animations: {
self.redBox.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 8.0/15.00, relativeDuration: 5.0/15.0, animations: {
self.redBox.alpha = 0.75
})
}) { (completed) in
print("completed")
}

UIView Animation Options Repeat count

I'm having some issue with my Swift code, I'm trying to make an UIImageView object fade away and reappear once, but having some issues with having the animation to play only once.
#IBOutlet weak var ball: UIImageView!
#IBAction func onFadeClick(_ sender: Any) {
UIView.animate(withDuration: 1, delay: 0.0, options: [.repeat, .autoreverse], animations: {
self.ball.alpha = 0.0
}, completion: nil)
}
I have attempted to read the documentation and previous questions but all have mentioned to use setAnimationRepeatCount, but xcode has an error stating that it is depreciated in iOS13 (which also does not work). Is there any built in functions I could use to stop the animation after it playing once? I have read somewhere saying using a callback function and reinitialize the animation but I am not exactly sure how to do that. Or is it a better idea to use UIViewPropertyAnimator instead of UIView.animate?
I'm just starting to learn Swift, any help or guidance is appreciated!
You can use the following to achieve your goal :
UIView.animateKeyframes(withDuration: 2, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
self.ball.alpha = 0.0
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
self.ball.alpha = 1
}
}) { (isFinished) in
}
Swift 5.x
The current iOS syntax replacement for deprecated approach could be writed like the example below:
UIView.animate(withDuration:1,
delay: 0.5,
options: [.curveEaseInOut, .autoreverse, .repeat]) {
UIView.modifyAnimations(withRepeatCount: 3, autoreverses: true) {
print("repeated stuff")
}
} completion: { _ in
print("done")
}
The iOS 13 replacement for the deprecated approach is https://developer.apple.com/documentation/uikit/uiview/3043564-modifyanimations. Example:
UIView.animate(withDuration:1, animations: {
UIView.modifyAnimations(withRepeatCount: 3, autoreverses: true, animations: {
// whatever
})
})
However, the deprecated approach does still work; it is just deprecated.
instead of the last }) use:
}, completion: { finished in
self.alpha = 1
})

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

blink UILabel text swift2 ios

I see code to make text in UILabel to blink, but I am using Swift 2, and what changes does one make to have such text blink in Swift?
I just need this style only to alert the user of my app to start the game, then I don't need any other text to blink.
If you have provided some code it were simpler to answer but without it it's I can do:
let foo = UITextField()
UIView.animateWithDuration(0.3, animations: {() -> Void in
foo.alpha = 0.0
},
completion: { finished in
UIView.animateWithDuration(0.3, animations: {
foo.alpha = 1.0
})
})
You can do this by using animateWithDuration.
Here i am writing in viewDidLoad(). you can use this according to your requirement.
override func viewDidLoad() {
super.viewDidLoad()
self.myLabel.alpha=0
UIView.animateWithDuration(1, delay: 0.2, options:[.Repeat,.Autoreverse],
animations:{ self.myLabel.alpha=1.0}, completion: nil)
}

Cocoa Animation not triggering when called by button

I'm developing an iOS app and i've been asked to add animations to make it more user friendly.
So i want to animate a badge on my button displaying quantity. When the quantity changes, my function valueForItemChanged is called, then i change the value in the bimButtonBadge label and i use a animation which makes its size bounce.
But I'm facing a problem: when i call .animateWithDuration() from an event triggered by a button, it doesn't work:
#IBAction func valueForItemChanged(sender: AnyObject) {
print("value changed");
self.bimButtonBadge.text = String(self.getTotalItemQuantity())
self.bimButtonBadge.transform = CGAffineTransformMakeScale(0.2, 0.2)
UIView.animateWithDuration(1.0,
delay: 0,
usingSpringWithDamping: 0.2,
initialSpringVelocity: 4.0,
options: UIViewAnimationOptions.AllowUserInteraction,
animations: {
self.bimButtonBadge.transform = CGAffineTransformIdentity
}, completion: nil)
}
So i tried to make this animation in viewDidLayoutSubviews(), just to see. It worked.
I think viewDidLayoutSubview is called from the main thread so i tried this :
#IBAction func valueForItemChanged(sender: AnyObject) {
print("value changed");
self.bimButtonBadge.text = String(self.getTotalItemQuantity())
self.bimButtonBadge.transform = CGAffineTransformMakeScale(0.2, 0.2)
dispatch_async(dispatch_get_main_queue(), {
UIView.animateWithDuration(1.0,
delay: 0,
usingSpringWithDamping: 0.2,
initialSpringVelocity: 4.0,
options: UIViewAnimationOptions.AllowUserInteraction,
animations: {
self.bimButtonBadge.transform = CGAffineTransformIdentity
}, completion: nil)
});
}
Aaaaaand, it worked, partially. Sometimes the label disappears. I searched on the web, but i couldn't find anything related to this. So i'm wondering where i'm wrong.
If anyone could answer me it would be really appreciated.
Thanks
Your animation (both versions) sets the badge's transform to the identity transform, but you don't show any code that sets the transform to anything other than the identity transform. An animation animates a change in a property. If you don't actually change anything, it won't animate.
What are you trying to do? You say you're using a bounce animation, but what do you mean? Bouncing the size? The position?
Problem solved.
I solved this by doing my first transformation in the main thread thanks to Duncan C.
My code is now :
#IBAction func valueForItemChanged(sender: AnyObject) {
print("value changed");
dispatch_async(dispatch_get_main_queue(), {
self.bimButtonBadge.text = String(self.getTotalItemQuantity())
self.bimButtonBadge.transform = CGAffineTransformMakeScale(0.2, 0.2)
UIView.animateWithDuration(1.0,
delay: 0,
usingSpringWithDamping: 0.2,
initialSpringVelocity: 4.0,
options: UIViewAnimationOptions.AllowUserInteraction,
animations: {
self.bimButtonBadge.transform = CGAffineTransformIdentity
}, completion: nil)
});
}

Resources