How to do a sequence animation on a textview (swift3) - ios

My code below is a button that when hit applies animation. Right now there are two animations. I would like to do a sequence the animations meaning have the 2nd animation not start until the first animation has completed.
var speed: CGFloat = 5.3 // speed in seconds
#IBAction func press(_ sender: Any) {
self.theTextView.resignFirstResponder()
UIView.animate(withDuration: TimeInterval(speed), animations: {
////1st action[
self.theTextView.contentOffset = .zero
self.theTextView.setContentOffset(.zero, animated: true)]
/////2nd action[
self.theTextView.contentOffset = CGPoint(x: 0, y: self.theTextView.contentSize.height)]
}, completion: nil)
}}

The easiest way of doing that would be using the completion block of the animate(withDuration:animations:completion:) method. The completion block is executed when the animation sequence ends. In your case, it would look like this :
UIView.animate(withDuration: 6.0, animations: {
// First animation goes here
self.theTextView.contentOffset = CGPoint.zero
}, completion: { (finished) in
// This completion block is called when the first animation is done.
// Make sure the animation was not interrupted/cancelled :
guard finished else {
return
}
UIView.animate(withDuration: 1.0, animations: {
// And the second animation goes here
self.theTextView.contentOffset = CGPoint(x: 0, y: self.theTextView.contentSize.height)
})
})

There is also convenient way to make sequence of concurrent animations by using animateKeyframes:
UIView.animateKeyframes(withDuration: 1, delay: 0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.3, animations: {
self.someView.alpha = 0.5
self.view.layoutIfNeeded()
})
//second animation
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.3, animations: {
self.someView.alpha = 1
self.view.layoutIfNeeded()
})
}, completion: { (finish) in
// Do something on animation complete
})

Related

how to get infinite loop? [duplicate]

I have a UIImageView which i want to rotate 180 degrees, taking up 1 second, then i want to wait 1 second at this position, then rotate 180 degrees back to the original position taking up 1 second.
How do i accomplish this? I've tried a 100 approaches and it keeps snapping back instead of rotating back
EDIT: I forgot to add i need this to repeat indefinitely
All you need to do is to create a keyFrame animations. It is designed to chain multiple animations together, and in the first keyframe rotate your UIImageView subclass by PI, and in the second one transform it back to identity.
let rotateForwardAnimationDuration: TimeInterval = 1
let rotateBackAnimationDuration: TimeInterval = 1
let animationDuration: TimeInterval = rotateForwardAnimationDuration + rotateBackAnimationDuration
UIView.animateKeyframes(withDuration: animationDuration, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: rotateForwardAnimationDuration) {
self.imageView.transform = CGAffineTransform(rotationAngle: .pi)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: rotateBackAnimationDuration) {
self.imageView.transform = .identity
}
})
Outcome:
EDIT:
Here is an example how to make it run indefinitely. I suppose your image is in a viewController, and you hold some reference to your imageView.
So for example, on viewDidAppear, call a function what triggers the animation, and than in the completion block of the animation, just call the same function again.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.runRotateAnimation()
}
func runRotateAnimation() {
let rotateForwardAnimationDuration: TimeInterval = 1
let rotateBackAnimationDuration: TimeInterval = 1
let animationDuration: TimeInterval = rotateForwardAnimationDuration + rotateBackAnimationDuration
UIView.animateKeyframes(withDuration: animationDuration, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: rotateForwardAnimationDuration) {
self.imageView.transform = CGAffineTransform(rotationAngle: .pi)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: rotateBackAnimationDuration) {
self.imageView.transform = .identity
}
}) { (isFinished) in
// To loop it continuosly, just call the same function from the completion block of the keyframe animation
self.runRotateAnimation()
}
}
}
You can perform the second animation in the completionHandler presented on UIView.animate
let duration = self.transitionDuration(using: transitionContext)
let firstAnimDuration = 0.5
UIView.animate(withDuration: firstAnimDuration, animations: {
/* Do here the first animation */
}) { (completed) in
let secondAnimDuration = 0.5
UIView.animate(withDuration: secondAnimDuration, animations: {
/* Do here the second animation */
})
}
Now you could have another problem.
If you rotate your view with the CGAffineTransform and for every animation you assign a new object of this type to your view.transform, you will lose the previous transform operation
So, according to this post: How to apply multiple transforms in Swift, you need to concat the transform operation
Example with 2 animation block
This is an example to made a rotation of 180 and returning back to origin after 1 sec:
let view = UIView.init(frame: CGRect.init(origin: self.view.center, size: CGSize.init(width: 100, height: 100)))
view.backgroundColor = UIColor.red
self.view.addSubview(view)
var transform = view.transform
transform = transform.rotated(by: 180)
UIView.animate(withDuration: 2, animations: {
view.transform = transform
}) { (completed) in
transform = CGAffineTransform.identity
UIView.animate(withDuration: 2, delay: 1, options: [], animations: {
view.transform = transform
}, completion: nil)
}
Example of .repeat animation and .autoreverse
The .animate method give you the ability to set some animation options. In particular the structure UIViewAnimationOptions contains:
.repeat, which repeat indefinitely your animation block
.autoreverse, which restore your view to the original status
With this in mind you could do this:
var transform = view.transform.rotated(by: 180)
UIView.animate(withDuration: 2, delay: 0, options: [.repeat, .autoreverse], animations: {
self.myView.transform = transform
})
But you need a delay between the two animations, so you need to do this trick:
Example of recursive animation and a delay of 1 sec
Just create a method inside your ViewController which animate your view. In the last completionHandler, just call the method to create a infinite loop.
Last you need to call the method on viewDidAppear to start the animation.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.animation()
}
func animation() {
var transform = view.transform
transform = transform.rotated(by: 180)
UIView.animate(withDuration: 2, delay: 0, options: [], animations: {
self.myView.transform = transform
}) { bool in
transform = CGAffineTransform.identity
UIView.animate(withDuration: 2, delay: 1, options: [], animations: {
self.myView.transform = transform
}, completion: { bool in
self.animation()
})
}
}

Animation not stoping using UIView.animate

My code is using a simple loop animation that is not stopping after the 5 second duration. All I want to do is have a second button that stops the animation. As you can see in my code, circle.stopAnimating() has no effect.
class VET: UIViewController {
#IBOutlet weak var circle: UIImageView!
var duration = 5
#IBAction func start(_ sender: Any) {
UIView.animate(withDuration: TimeInterval(duration), delay: 0.5, options: [.repeat, .autoreverse, .curveEaseIn, .curveEaseOut], animations: { () -> Void in
let scale = CGAffineTransform(scaleX: 0.25, y: 0.25)
self.circle.transform = scale
print("animation")
}, completion: { _ in
//if finished { or (finished: Bool)
// if isAnimating {
if self.circle.isAnimating {
self.circle.stopAnimating()
print("is animating -- stop animating")
} else {
self.circle.startAnimating()
print("start animating")
}
})
}
#IBAction func stop() {
circle.stopAnimating()
}
startAnimating and stopAnimating for change images of imageview in some interval like GIF. Here you used animation block with repeat and the completion block will only get called when the animation is interrupted. For example it gets called when the app goes in the background and comes back to the foreground again.
To stop animation after some interval you have to call forcefully removeAllAnimations after that interval. Please refer following code:
#IBOutlet weak var circle: UIImageView!
var duration = 10
func start() {
UIView.animate(withDuration: TimeInterval(duration), delay: 0.5, options: [.repeat, .autoreverse, .curveEaseIn, .curveEaseOut], animations: { () -> Void in
let scale = CGAffineTransform(scaleX: 0.25, y: 0.25)
self.circle.transform = scale
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.duration * 1000)) {
// Code
self.circle.layer.removeAllAnimations()
}
print("animation")
}, completion: { _ in
let scale = CGAffineTransform(scaleX: 1.0, y: 1.0)
self.circle.transform = scale
})
}
You are using wrong approach to use animations if you need more control over it.
Instead use UIViewPropertyAnimator
The alternate property animator initializer gives you back an animator object that you need to start:
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
self.redSquare.backgroundColor = .green
}
animator.startAnimation()
Similarly you can use for stopping animations related to the animator
animator.stopAnimation()

Animation completion block goes haywire after dismissing view

I have a detail viewcontroller that contains a repeating carousel with an animating image view like so:
func animateCarousel1(){
UIView.animate(withDuration: 1, delay: 3, options: .curveEaseInOut, animations:
//image animation
},completion: { (_) -> Void in
print("animation 1 complete")
self.animateCarousel2()
})
}
func animateCarousel2(){
UIView.animate(withDuration: 1, delay: 3, options: .curveEaseInOut, animations:
//image animation
},completion: { (_) -> Void in
print("animation 2 complete")
self.animateCarousel1()
})
}
Upon popping this view and returning to the parent view, I see that in the debug console, the functions are continuing to be called in the background, repeatedly and simultaneously, infinitely.
CPU usage also jumps to 90% in the simulator as well.
Is there some sort of deinit I need to do before popping the viewcontroller? I can't seem to wrap my head around this one.
The issue is that completion blocks are called even if the animation finishes. So there are a few possible solutions:
Check the finished parameter of the completion handlers:
func animateCarousel1() {
UIView.animate(withDuration: 1, delay: 3, options: .curveEaseInOut, animations: {
//image animation
}, completion: { finished in
print("animation 1 complete")
if finished { self.animateCarousel2() }
})
}
func animateCarousel2() {
UIView.animate(withDuration: 1, delay: 3, options: .curveEaseInOut, animations: {
//image animation
}, completion: { finished in
print("animation 2 complete")
if finished { self.animateCarousel1() }
})
}
Use a different animation technique that doesn't require the circular references between these animation routines, e.g.:
func animateCarousel() {
UIView.animateKeyframes(withDuration: 8, delay: 3, options: .repeat, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.125) {
// animation 1
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.125) {
// animation 2
}
}, completion: nil)
}
It's possible this solve the problem you can see this post link here
Use on deinit() controller method or viewDidDissapear(), try the following code
deinit {
print("deinit \(NSStringFromClass(self.classForCoder).components(separatedBy: ".").last ?? "")") // print viewcontroller deallocated
self.view_for_remove_animations.layer.removeAllAnimations() // Forze delete animations
}
OR
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("viewDidDisappear \(NSStringFromClass(self.classForCoder).components(separatedBy: ".").last ?? "")") // print viewcontroller deallocated
self.view_for_remove_animations.layer.removeAllAnimations() // Forze delete animations
}
Try it, and send feedback
The reason is in animation block, you are using strong reference of self which is why when you are poping this viewcontroller its reference count is still not 0, because of which ARC is not able to dealloc the reference of this view controller.
There is a concept of [weak self]. You can modify your code like below and then after poping you won't see those method calls in your debug console. Reason: weak self will not increase the reference count and ARC will be able to remove the reference of the object
func startAnimation() {
UIView.animate(withDuration: 0.4, animations: {[weak self] in
self?.animateView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
print("startAnimation")
}, completion: {[weak self]
finished in
self?.startAnimation2()
})
}
func startAnimation2() {
UIView.animate(withDuration: 0.4, animations: {[weak self] in
self?.animateView.frame = CGRect(x: 100, y: 0, width: 100, height: 100)
print("startAnimation2")
}, completion: {[weak self]
finished in
self?.startAnimation()
})
}

High CPU Usage because of UIView.animate and UIView.animateKeyframes

I have two view controllers. AnimationVC has some UIView animations, and DestinationVC has none.
I have a CPU usage problem. After I perform the segue, the animation blocks still show up in the Instruments, even though these lines belong to the AnimationVC that performs the segue.
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveEaseInOut, .autoreverse, .repeat], animations: {
self.s1.alpha = 0.0
self.s3.alpha = 0.0
}, completion: nil)
and
let dur = 0.5/12
UIView.animateKeyframes(withDuration: 30.0, delay: 0.0, options: [.repeat, .calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: dur, animations: {
self.sc.alpha = 1.0
})
UIView.addKeyframe(withRelativeStartTime: 1.0-dur, relativeDuration: dur, animations: {
self.sc.alpha = 0.0
})
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
self.sc.transform = CGAffineTransform(rotationAngle: .pi*0.2)
})
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
self.sc.center.y = 0.1*self.frame.size.height
})
}, completion: nil)
I tried calling this destruct function...
func destruct(){
layer.removeAllAnimations()
subviews.forEach { $0.removeFromSuperview() }
}
...in
just before performing the segue
in the AnimationVC's deinit method
Still, it shows alive and hogs the CPU with 40% load. How can I destruct this?
I even reset the navigation stack on the DestinationVC with...
self.navigationController?.viewControllers = [self]
...and I see the lines of deinit from AnimationVC, the animation delay closure is still alive.
As it turns out
...
}) { (finished) in
if finished {
self.specialAniamtion(delay: 2.0)
}
}
and calling destruct() on deinit fixed it.
func destruct(){
layer.removeAllAnimations()
}

animateWithDuration one after another

I have a label that is initially positioned at the center of the screen. It currently transitions from the the center to the right end of the screen then autoreverses back to its position. I'd like to have it begin another animateWithDuration so that it continues from the center return to the left position of the screen then autoreverse back to the position and sequentially loop from there on after.
I have already attempted and successfully made the first half work but I'm not sure how to continue to the second portion where it begins the center->left transition and loop.
Swift 2.0 Code:
func animateRight()
{
UIView.animateWithDuration(1.0, delay: 0.0, options: [ .Autoreverse, .CurveEaseInOut], animations: {
label.center.x = self.view.frame.width/2
}, completion: { finished in
if finished {
label.frame.origin.x = 0.0
animateLeft()
}
})
}
func animateLeft()
{
UIView.animateWithDuration(1.0, delay: 0.0, options: [ .Autoreverse, .CurveEaseInOut], animations: {
label.frame.origin.x = (self.view.frame.width/2) * -1
}, completion: { finished in
if finished {
label.center.x = self.view.frame.width/2
animateRight()
}
})
}
// Start process
animateRight()
You should call the same animate with duration method you create for animating to right and call in completion. Something like this:
func animateRight()
{
UIView.animate(withDuration: 1.0, delay: 0.0, options: [], animations: {
self.label.center.x = self.view.frame.width
}, completion: { finished in
if finished {
self.animateLeft()
}
})
}
func animateLeft()
{
UIView.animate(withDuration: 2.0, delay: 0.0, options: [ .autoreverse, .repeat, .curveEaseInOut, .beginFromCurrentState], animations: {
self.label.frame.origin.x = 0.0
}, completion: nil)
}
Call defaultSetup to start
When animationRight end, it will call animationLeft
When animationLeft end, it will call animationRight again to run animation loop
Here is the code:
func defaultSetup(){
self.animationRight()
}
func animationRight(){
let transform = CGAffineTransform(scaleX: 1.05, y: 1.05).rotated(by: .pi/180)
UIView.animate(withDuration: 3.0, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3.0, options: [.allowUserInteraction, .repeat], animations: { [weak self] in
guard let `self` = self else { return }
self.vRight.transform = transform
}, completion: { [weak self] (done) in
guard let `self` = self else { return }
self.vRight.transform = .identity
self.animationLeft()
})
}
func animationLeft(){
let transform = CGAffineTransform(scaleX: 1.05, y: 1.05).rotated(by: .pi/180)
UIView.animate(withDuration: 3.0, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3.0, options: [.allowUserInteraction], animations: { [weak self] in
guard let `self` = self else { return }
self.vLeft.transform = transform
}, completion: { [weak self] (done) in
guard let `self` = self else { return }
self.vLeft.transform = .identity
self.animationRight()
})
}

Resources