0
votes

I have a horizontal UIScrollview in my app which has 1 really long UIImageView to start with. I have a timer and animation to create an illusion that the image under scroll view is automatically scrolling. Once the image comes to an end i dynamically add similar image to the scroll view so it should look like the image is repeating itself.

This is how i want them to be displayed under scroll view : image1|image2|image3|image4...... and these images will be scrolling from right to left. Exactly how it works in Behance's iphone app before you login.

Here's the code i have (in storyboard i have the scroll view and one UIIMageview already added).

override func viewDidAppear(_ animated: Bool) {

        Timer.scheduledTimer(timeInterval: 0.6, target: self, selector: #selector(scrollImage), userInfo: nil, repeats: true)
    }

    @objc func scrollImage() {
        offSet.x = offSet.x + CGFloat(20)
        UIView.animate(withDuration: 1.0, animations: {
            self.behanceView.setContentOffset(self.offSet, animated: false)
        })
    }

     func addImagetoScrollView() {
        let imageView = UIImageView(image:UIImage(named:"Landing_Scrollable"))
        print(imageCount*Int(self.behanceView.contentSize.width)+100)
        imageView.frame = CGRect(x:imageCount*Int(self.behanceView.contentSize.width), y: 0, width: 875, height: 502)
        self.behanceView.contentSize = CGSize(width: imageView.bounds.size.width * CGFloat(imageCount), height: imageView.bounds.size.height)
        self.behanceView.addSubview(imageView)
        imageCount+=1
    }

    extension ViewController: UIScrollViewDelegate {
        func scrollViewDidScroll(_ scrollView: UIScrollView) {

            let scrollViewWidth = scrollView.frame.size.width;
            let scrollOffset = scrollView.contentOffset.x;
            print(imageCount*Int(self.behanceView.contentSize.width - scrollViewWidth))
            if scrollOffset >= CGFloat(imageCount*Int(self.behanceView.contentSize.width - scrollViewWidth)) {
                self.addImagetoScrollView()
            }
        }
    }

But when i see it in action, it does something wierd and animation is all off.

Can someone please help.

Thanks,

1
Do you want the user to be able to manually scroll the view? Or should it only scroll because of the timer? - rob mayoff
I think you should add all image to scroll view before you scroll image instead of call scroll image in viewDidAppear, btw you should call super.viewDidAppear(animated) at top of its function - Tung Vu Duc
@robmayoff : I want this to be an automatically scrolled based on the timer and user interaction is disabled. - Ashutosh
@TungVuDuc : Good catch. I can add all the images in the beginning and that works but i cannot be sure how many images are going to be good enough so want to dynamically add this. - Ashutosh

1 Answers

2
votes

I've never seen the “Behance” app, but I guess you're asking how to animate a seamlessly tiled background image across the screen indefinitely, like this:

demo

(Pattern image by Evan Eckard.)

I used an animation duration of 1 second for the demo, but you probably want a much longer duration in a real app.

You shouldn't use a timer for this. Core Animation can perform the animation for you, and letting it perform the animation smoother and more efficient. (You might think Core Animation is performing your animation since you're using UIView animation, but I believe animating a scroll view's contentOffset does not use Core Animation because the scroll view has to call its delegate's scrollViewDidScroll on every animation step.)

You also shouldn't use a scroll view for this. UIScrollView exists to allow the user to scroll. Since you're not letting the user scroll, you shouldn't use UIScrollView.

Here's how you should set up your background:

  1. Create two identical image views (numbered 0 and 1), showing the same image. Make sure the image views are each big enough to fill the screen.

  2. Put the left edge of image view 0 at the left edge of your root view. Put the left edge of image view 1 at the right edge of image view 0. Since each image view is big enough to fill the screen, image view 1 will start out entirely off the right edge of the screen.

  3. Animate image view 0's transform.translation.x from 0 to -imageView.bounds.size.width. This will make it slide to the left by precisely its own width, so when the animation reaches its end, image view 0's right edge is at the left edge of the screen (and thus image view 0 is entirely off the left edge of the screen). Set the animation's repeatCount to .infinity.

  4. Add the same animation to image view 1. Thus image view 1 comes onto the screen as image view 0 is leaving it, exactly covering the pixels revealed by image view 0's animation.

The two animations end at exactly the same time. When they end, image view 1 is exactly where image view 0 was at the start. Since both animations are set to repeat infinitely, they both immediately start over. When image view 0's animation starts over, image view 0 instantly jumps back to its starting position, which is where image view 1 ended up. Since both image views show the same image, the pixels on screen don't change. This makes the animation loop seamless.

Here's my code:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        for imageView in imageViews {
            imageView.image = patternImage
            imageView.contentMode = .scaleToFill
            view.addSubview(imageView)
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let bounds = view.bounds

        let patternSize = patternImage.size
        // Scale the image up if necessary to be at least as big as the screen on both axes.
        // But make sure scale is at least 1 so I don't shrink the image if it's larger than the screen.
        let scale = max(1 as CGFloat, bounds.size.width / patternSize.width, bounds.size.height / patternSize.height)
        let imageFrame = CGRect(x: 0, y: 0, width: scale * patternSize.width, height: scale * patternSize.height)

        for (i, imageView) in imageViews.enumerated() {
            imageView.frame = imageFrame.offsetBy(dx: CGFloat(i) * imageFrame.size.width, dy: 0)

            let animation = CABasicAnimation(keyPath: "transform.translation.x")
            animation.fromValue = 0
            animation.toValue = -imageFrame.size.width
            animation.duration = 1
            animation.repeatCount = .infinity
            animation.timingFunction = CAMediaTimingFunction(name: .linear)

            // The following line prevents iOS from removing the animation when the app goes to the background.
            animation.isRemovedOnCompletion = false

            imageView.layer.add(animation, forKey: animation.keyPath)
        }
    }

    private let imageViews: [UIImageView] = [.init(), .init()]
    private let patternImage = #imageLiteral(resourceName: "pattern")

}