简体   繁体   中英

How to make loading animation in iOS Swift?

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:

在此处输入图像描述

Expected Animation:

在此处输入图像描述

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

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:

在此处输入图像描述

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.

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.

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

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM