Updated 5 days ago | GitHub

Simulated Infinite Scrolling

In this guide, we will simulate an Infinite Scrolling experience. Simulated Infinite Scrolling allows a user to continually scroll through content, pausing briefly at the bottom of each scroll to simulate network loading. To achieve this effect, we will leverage the following concepts:

infinite scroll demo|220

Step 1: Position the UIActivityIndicatorView

We will put a UIActivityIndicatorView inside the UIScrollView, directly in the middle. We set its initial position when the ViewController loads. It will always be animating and will only be revealed when we scroll to the very bottom. After a delay, a new ImageView will be added and the Activity Indicator will be moved down below the new ImageView.

  • Add an Activity Indicator to Storyboard inside of the ScrollView.
  • Set Behavior to Animating, in the Attributes Inspector for the Activity Indicator.
  • Create an outlet for the Activity Indicator. We will name ours, infiniteLoadingIndicator.
  • Set the Activity Indicator position within the viewDidLoad method. We will place it 20px below the feed ImageView. Base the position on the ImageView’s frame (where the view actually sits in the ScrollView), not on its image’s size. A UIImage‘s size is the image’s own logical size in points, and the ImageView can scale the image to fit its bounds depending on its content mode, so the two match only when the frame is exactly the size of the image. Using the frame also avoids force-unwrapping image, which would crash if the image didn’t load.
  • Set the ScrollView Content Inset. In order for the ScrollView to scroll up enough to reveal our Activity Indicator, we need to set the content Inset for the bottom. You can do this in the ScrollView’s Size Inspector, but since we are already setting things up in viewDidLoad, we will set the Content Inset there programmatically.
   // Position the Activity Indicator 20px below the bottom of the feed ImageView
   infiniteLoadingIndicator.center.y = feedImageView.frame.origin.y + feedImageView.frame.size.height + 20

   // Set the ScrollView bottom Content Inset to 130px
   feedScrollView.contentInset.bottom = 130

Step 2: Register for Scroll Events

We will need to interface with our ScrollView more programmatically in order to get contentOffset data as well as call a special ScrollView method when our ScrollView has come to a stop. To get this information from our ScrollView, we will first need to register for Scroll Events.

Step 3: Create a Variable to Keep Track Of ImageViews

We will be creating a new ImageView each time we scroll to the bottom of our ScrollView. In order to figure out where we should place the next ImageView as well as how much to adjust the contentSize of our ScrollView, we will need to keep track of how many ImageViews have been created. We will do this in a variable called, numberOfImageViews.

  • Create a “global” variable, (up where your outlets are), to keep track of how many ImageViews have been added. Give it an initial value of 1.
  • Create a second variable, isMoreDataLoading, to remember whether a new ImageView is already on its way. Give it an initial value of false. Step 4 uses it to ignore extra stops at the bottom while the delay is still running.
   // Define variable to keep track of added ImageViews.
   // Initial Value of 1 represents the initial ImageView
   var numberOfImageViews: CGFloat = 1

   // True while we are waiting to add the next ImageView
   var isMoreDataLoading = false

Step 4: Create and Configure a Method for When Scrolling has Stopped.

  • We will employ a special ScrollView method, scrollViewDidEndDecelerating to tell us when the Scroll has come to a complete stop. You can put this method anywhere in the ViewController. Implement the Desired Event

  • When the ScrollView stops, we need to check if the ScrollView has indeed reached the bottom. We will use a conditional statement and the contentOffset of the ScrollView to help us tell if the ScrollView has scrolled all the way to the bottom.

  • UIKit calls scrollViewDidEndDecelerating each time a decelerating scroll comes to a halt. (If the user lifts their finger without any deceleration, UIKit calls scrollViewDidEndDragging(_:willDecelerate:) with false instead, and this method isn’t called.) That means a second flick can decelerate to a stop at the bottom before the delay below has finished. No new content has been added yet, so the bottom check passes a second time. To avoid scheduling a second ImageView, we only start loading when isMoreDataLoading is false, and we set it to true before the delay starts.

  • We will run a block after a delay using DispatchQueue.main.asyncAfter so that the Activity Indicator gets a moment to animate before we add the next ImageView to the ScrollView Content.

  • When the delay is finished, we will programmatically create a new Image View, put another copy of our Feed Image in it, and position it directly below the previous ImageView. We will reference the variable we created to keep track of how many ImageViews have been created and multiply it by the height of our ImageView to figure out how far down to place it each time.

  • Move the Activity Indicator down below the new ImageView that was created.

  • Since new ImageViews have been added to our ScrollView, we need to increase the contentSize of our ScrollView to accommodate the new ImageView. Again, we will reference our numberOfImageViews variable, multiply it by the height of our new ImageView, and add that to the original contentSize.

  • Finally, we will increase the value of our numberOfImageViews variable by 1 and set isMoreDataLoading back to false, so the next stop at the new bottom can load another ImageView.

    // The scrollView has come to a complete stop, so do the following...
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print("Scrolling Stopped")
        // If the scrollView offset + the scrollview height is greater than or equal to the height of the scrollView content,
        // We have reached the bottom, so...
        if !isMoreDataLoading && scrollView.contentOffset.y + scrollView.frame.size.height >= scrollView.contentSize.height {
            print("You reached the bottom")
            // Ignore further stops at the bottom until the new ImageView has been added
            isMoreDataLoading = true
            // Delay for 1 second then...
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                // Create frame for new imageView, same as feed ImageView
                let newFrame = self.feedImageView.frame
                // Create new ImageView with newFrame
                let newImageView = UIImageView(frame: newFrame)
                // Create new image, same as feed Image
                let newImage = UIImage(named: "home_feed")
                // Set new ImageView image to newImage
                newImageView.image = newImage
                // Position the new imageView below the previous image view
                newImageView.frame.origin.y = self.feedImageView.frame.origin.y + self.numberOfImageViews * self.feedImageView.frame.size.height
                // Add imageView to scrollView
                scrollView.addSubview(newImageView)
                // Move Activity Indicator down below new ImageView
                self.infiniteLoadingIndicator.center.y = newImageView.frame.origin.y + newImageView.frame.size.height + 20
                // Increase the scrollView contentSize with each additional imageView added using
                scrollView.contentSize = CGSize(width: self.feedImageView.frame.size.width,
                    height: self.feedImageView.frame.size.height + self.numberOfImageViews * newImageView.frame.size.height)
                // add 1 to numberOfImageViews
                self.numberOfImageViews += 1
                // Allow the next stop at the bottom to load another ImageView
                self.isMoreDataLoading = false
            }
        }
    }

Using with UITableView

To see how to implement infinite scrolling effects using UITableViews, see this guide.