I want to create a looping animation like this GIF:
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?
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?
We can use the same approach... the "trick" is to duplicate the first image at the end:
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 :)