1

Hi I want to make animation with 3 UIView. The main problem is I can't start animation once it's stopped by removing animation from layer.

Here is the code:

class HTAnimatedTypingView: UIView {

    @IBOutlet weak var view1: UIView!
    @IBOutlet weak var view2: UIView!
    @IBOutlet weak var view3: UIView!

    func startAnimation(){

        UIView.animate(withDuration: 0.5, delay: 0, options: .repeat, animations: {

        self.view1.frame.origin.y = 0

       }, completion: nil)

        UIView.animate(withDuration: 0.3, delay: 0.5, options: .repeat, animations: {

               self.view2.frame.origin.y = 0

              }, completion: nil)

        UIView.animate(withDuration: 0.2, delay: 1.0, options: .repeat, animations: {

                     self.view3.frame.origin.y = 0

                    }, completion: nil)

    }

    func stopAnimations(){

        self.view1.layer.removeAllAnimations()
        self.view2.layer.removeAllAnimations()
        self.view3.layer.removeAllAnimations()

    }
}

Output of Above Code:

enter image description here

Expected Animation:

enter image description here

How can make it work with start animation & stop animation functionality? Thanks in advance...

5 Answers 5

6

Since you need to add some pause in between each sequence of animations, I would personally do it using key frames as it gives you some flexibility:

class AnimationViewController: UIViewController {

    private let stackView: UIStackView = {
        $0.distribution = .fill
        $0.axis = .horizontal
        $0.alignment = .center
        $0.spacing = 10
        return $0
    }(UIStackView())

    private let circleA = UIView()
    private let circleB = UIView()
    private let circleC = UIView()
    private lazy var circles = [circleA, circleB, circleC]

    func animate() {
        let jumpDuration: Double = 0.30
        let delayDuration: Double = 1.25
        let totalDuration: Double = delayDuration + jumpDuration*2

        let jumpRelativeDuration: Double = jumpDuration / totalDuration
        let jumpRelativeTime: Double = delayDuration / totalDuration
        let fallRelativeTime: Double = (delayDuration + jumpDuration) / totalDuration

        for (index, circle) in circles.enumerated() {
            let delay = jumpDuration*2 * TimeInterval(index) / TimeInterval(circles.count)
            UIView.animateKeyframes(withDuration: totalDuration, delay: delay, options: [.repeat], animations: {
                UIView.addKeyframe(withRelativeStartTime: jumpRelativeTime, relativeDuration: jumpRelativeDuration) {
                    circle.frame.origin.y -= 30
                }
                UIView.addKeyframe(withRelativeStartTime: fallRelativeTime, relativeDuration: jumpRelativeDuration) {
                    circle.frame.origin.y += 30
                }
            })
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        circles.forEach {
            $0.layer.cornerRadius = 20/2
            $0.layer.masksToBounds = true
            $0.backgroundColor = .systemBlue
            stackView.addArrangedSubview($0)
            $0.widthAnchor.constraint(equalToConstant: 20).isActive = true
            $0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        animate()
    }
}

It should be pretty straightforward, but feel free to let me know if you have any questions!

And this is how the result looks like:

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

2

One way could be to use a Timer. Keep an instance of Timer in your class. When startAnimation is called, schedule it. When stopAnimation is called, invalidate it. (This means that the currently ongoing animation will be completed before the animation actually stops, which IMO makes it a nice non-abrupt stop).

On each tick of the timer, animate the dots once. Note that the animation you apply on each dot should have the same duration, as in the expected output, they all bounce at the same rate, just at different instants in time.

Some illustrative code:

// startAnimation
timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { _ in
    self.animateDotsOnce()
}

// stopAnimation
timer.invalidate()

// animateDotsOnce
UIView.animate(withDuration: animationDuration, delay: 0, animations: {

    self.view1.frame.origin.y = animateHeight

}, completion: {
    _ in
    UIView.animate(withDuration: animationDuration) {
        self.view1.frame.origin.y = 0
    }
})

// plus the other two views, with different delays...

I'll leave it to you to find a suitable animateHeight, timerInterval, animationDuration and delays for each view.

3 Comments

Thank you!, Works good!; I set 0.2, 0.4 delay to view2, view3 and timer interval is 0.5(animation duration)+delays = 1.1; This works what I expected!
I would definitely not recommend using Timers for such a task while it can all be done very easily and in a more performant way via the dedicated API: UIView.animateKeyframes.
@AlexLinares Yeah, you're right... I'm not familiar with animateKeyframes...
2

I'd recommend using a CAKeyframeAnimation instead of handling completion blocks and that sorcery. Here's a quick example:

for i in 0 ..< 3 {
    let bubble = UIView(frame: CGRect(x: 20 + i * 20, y: 200, width: 10, height: 10))
    bubble.backgroundColor = .red
    bubble.layer.cornerRadius = 5
    self.view.addSubview(bubble)

    let animation = CAKeyframeAnimation()
    animation.keyPath = "position.y"
    animation.values = [0, 10, 0]
    animation.keyTimes = [0, 0.5, 1]
    animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    animation.duration = 1
    animation.isAdditive = true
    animation.repeatCount = HUGE
    animation.timeOffset = CACurrentMediaTime() + 0.2 * Double(i)

    bubble.layer.add(animation, forKey: "anim")
}

When you wanna remove the animation you just use bubble.layer.removeAnimation(forKey: "anim"). You might have to play around with the timing function or values and keyTimes to get the exact movement you want. But keyframes is the way to go to make a specific animation.

Side note: this example won't work in viewDidLoad cause the view doesn't have a superview yet so the animation won't work. If you test it in viewDidAppear it will work.

Comments

2

UIView animation is different to CALayer animation,best not to mix them. Write locally and tested.

import UIKit
import SnapKit

class HTAnimatedTypingView: UIView {

    private let view0 = UIView()
    private let view1 = UIView()
    private let view2 = UIView()

    init() {
        super.init(frame: CGRect.zero)
        makeUI()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        makeUI()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        makeUI()
    }

    private func makeUI() {

        backgroundColor = UIColor.white

        view0.backgroundColor = UIColor.red
        view1.backgroundColor = UIColor.blue
        view2.backgroundColor = UIColor.yellow

        addSubview(view0)
        addSubview(view1)
        addSubview(view2)

        view0.snp.makeConstraints { (make) in
            make.centerY.equalTo(self.snp.centerY)
            make.width.equalTo(10)
            make.height.equalTo(10)
            make.left.equalTo(self.snp.left)
        }

        view1.snp.makeConstraints { (make) in
            make.centerY.equalTo(self.snp.centerY)
            make.width.equalTo(10)
            make.height.equalTo(10)
            make.centerX.equalTo(self.snp.centerX)
        }

        view2.snp.makeConstraints { (make) in
            make.centerY.equalTo(self.snp.centerY)
            make.width.equalTo(10)
            make.height.equalTo(10)
            make.right.equalTo(self.snp.right)
        }

    }

    public func startAnimation() {

        let duration:CFTimeInterval = 0.5
        let animation_delay:CFTimeInterval = 0.1

        assert(duration >= animation_delay * 5, "animation_delay should be way smaller than duration in order to make animation natural")

        let translateAnimation = CABasicAnimation(keyPath: "position.y")
        translateAnimation.duration = duration
        translateAnimation.repeatCount = Float.infinity
        translateAnimation.toValue = 0
        translateAnimation.fillMode = CAMediaTimingFillMode.both
        translateAnimation.isRemovedOnCompletion = false
        translateAnimation.autoreverses = true

        view0.layer.add(translateAnimation, forKey: "translation")
        DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay) { [unowned self ] in
            self.view1.layer.add(translateAnimation, forKey: "translation")
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay * 2) { [unowned self ] in
            self.view2.layer.add(translateAnimation, forKey: "translation")
        }

    }

    public func stopAnimation() {
        self.view0.layer.removeAllAnimations()
        self.view1.layer.removeAllAnimations()
        self.view2.layer.removeAllAnimations()
    }

}

Comments

0
private func animateDots(circles: [UIView]){
        UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseInOut], animations: {
            circles[0].transform = CGAffineTransform(scaleX: 1.8, y: 1.8)
         }) { (finished) in
             UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseInOut], animations: {
                 circles[0].transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                 circles[1].transform = CGAffineTransform(scaleX: 1.8, y: 1.8)
              }) { (finished) in
                  UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseInOut], animations: {
                      circles[1].transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                      circles[2].transform = CGAffineTransform(scaleX: 1.8, y: 1.8)
                   }) { (finished) in
                       UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseInOut], animations: {
                           circles[1].transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                           circles[2].transform = CGAffineTransform(scaleX: 1.8, y: 1.8)
                        }) { (finished) in
                            UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseInOut], animations: {
                                circles[2].transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                             }) { (finished) in
                                 self.animateDots(circles: circles)
                            }
                       }
                  }
             }
        }
    }

3 Comments

Please don't post code-only answers. Good answers explain how they solve the question's issue.
@HangarRash The code explains the logic itself, rather than creating ongoing 3 timers, it would be best to use recursive function. Here the series of animations are called post completion of one animation. If you find it difficult to comprehend, then I can give more explanation.
I can read and understand your code just fine. That's not the point. As I said, a good answer provides some explanation. A good answer addresses the issues in the question in addition to providing a possible code solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.