iosswiftuikit

How to create loop animation with image?


I want to create a looping animation like this GIF:

enter image description here

I have one picture, these is clouds. I want to see an animation of looping clouds when I launch the app. I'm adding 2 imageView with the same cloud picture. I use this code to do this:

cloudsImageView1.frame.origin.x = 0
cloudsImageView2.frame.origin.x = screenSize
        
UIView.animate(withDuration: 20.0, delay: 0.0, options: [.repeat, .curveLinear], animations: {
    self.cloudsImageView1.frame = self.cloudsImageView1.frame.offsetBy(dx: -1 * screenSize, dy: 0.0)
            
    self.cloudsImageView2.frame = self.cloudsImageView2.frame.offsetBy(dx: -1 * screenSize, dy: 0.0)
}, completion: nil)

But I have bad results with wrong direction and disappearing images. How to fix it?


Solution

  • We can do this by adding the image views as subviews of a UIView, and then animate that "container" view.

    Since we are using a single image, we can run a repeating animation. When the animation "re-starts," it will reset the container view's .origin.x to 0.0 and it will appear as a seamless, infinite scroll.

    Quick example code:

    class ScrollingCloudsViewController: UIViewController {
        
        let imageContainerView = UIView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            
            // this can be called multiple times
            //  so we only want to setup and add the views once
            
            if imageContainerView.superview == nil {
                
                guard let img = UIImage(named: "singleCloud") else { return }
                
                // full width of view
                let w = view.frame.width
                
                // whatever height you want to use
                let h = 240.0
                
                // create two image views with the same image
                let v1 = UIImageView(image: img)
                let v2 = UIImageView(image: img)
                
                // set them up
                v1.frame = .init(x: 0.0, y: 0.0, width: w, height: h)
                v2.frame = .init(x: w, y: 0.0, width: w, height: h)
                
                imageContainerView.addSubview(v1)
                imageContainerView.addSubview(v2)
                
                imageContainerView.frame = .init(x: 0.0, y: 0.0, width: w * 2.0, height: h)
                view.addSubview(imageContainerView)
                
            }
            
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            // full width of view
            let w = view.frame.width
            
            UIView.animate(withDuration: 20.0, delay: 0.0, options: [.repeat, .curveLinear], animations: { [weak self] in
                guard let self else { return }
                self.imageContainerView.frame.origin.x = -w
            }, completion: { _ in
                // if we want to do anything on completion
            })
        }
        
    }
    

    Edit

    The above works fine with a single image. But, what if we want the same continuous scrolling with a series of images, such as these?

    frame1

    frame2

    frame3

    We can use the same approach... the "trick" is to duplicate the first image at the end:

    a

    b

    c

    d

    When the animation loop finishes, our first image will be in position to be replaced by, well, our first image again.

    Here is a modified version of the above code that will handle multiple images:

    class MultiScrollingViewController: UIViewController {
        
        let imageNames: [String] = [
            "frame1", "frame2", "frame3",
        ]
        
        let imageContainerView = UIView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .systemBackground
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            print(#function)
            
            // this can be called multiple times
            //  so we only want to setup and add the views once
    
            if imageContainerView.superview == nil {
                
                // full width of view
                let w = view.frame.width
                
                // whatever height you want to use
                let h = 200.0
    
                var x: CGFloat = 0.0
                
                for imgName in imageNames {
                    let v = UIImageView()
                    if let img = UIImage(named: imgName) {
                        v.image = img
                    }
                    v.frame = .init(x: x, y: 0.0, width: w, height: h)
                    imageContainerView.addSubview(v)
                    x += w
                }
                let v = UIImageView()
                if let imgName = imageNames.first, let img = UIImage(named: imgName) {
                    v.image = img
                }
                v.frame = .init(x: x, y: 0.0, width: w, height: h)
                imageContainerView.addSubview(v)
                x += w
    
                imageContainerView.frame = .init(x: 0.0, y: 0.0, width: x, height: h)
    
                view.addSubview(imageContainerView)
            }
            
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            let w = view.frame.width
            
            // we need to move the width of the number of "frames" minus 1
            let dist: CGFloat = w * CGFloat(imageContainerView.subviews.count - 1)
            
            // speed is how long - in seconds - we want the animation
            //  to take to move one "frame" to the left
            let speed: Double = 10.0
            
            // total duration is speed * the number of "frames" minus 1
            let totalDur: Double = speed * CGFloat(imageContainerView.subviews.count - 1)
            
            UIView.animate(withDuration: totalDur, delay: 0.0, options: [.repeat, .curveLinear], animations: { [weak self] in
                guard let self else { return }
                self.imageContainerView.frame.origin.x = -dist
            }, completion: { _ in
                // we're not doing anything on completion
            })
        }
        
    }
    

    Note: neither of the above classes take into consideration resizing ... for example, if we rotate the device and want the scrolling images to fill the new width. That would get a little trickier, as we would probably want to keep the current position, and we may not want the images to be stretched that much.

    But... that's another task to work on :)