Building a Infinitely-Scrolling gallery with a UICollectionView

Jul 2, 2013 00:00 · 1167 words · 6 minute read

A common requirement for a photo gallery or similar is endless scrolling - once you’ve reached the end of the list of items, the list “wraps around” and begins again from the first item. UICollectionViews very easily lend themselves to creating galleries, but there’s no in-built mechanism for endless scrolling.

This is a technique that can create the illusion of endless scrolling with relatively little effort. It uses UICollectionView’s scrollToItemAtIndexPath:atScrollPosition:animated: method to move the collection view back around to the beginning when it’s scrolled to the end of a list of items - by setting the animation property to NO, the move takes place without the user being aware of it.

By implementing the same effect in reverse when the collection view reaches the beginning, it appears that it can be infinitely scrolled in either direction.

Three assumptions first - the collection view is scrolling horizontally; the cells fill the full width of the collection view’s frame; and the collection view has its paging property set to YES so that cells will ‘snap’ to the edges of the frame as they scroll.

Setting up the data

The technique relies on manipulating the list of items to add an extra item at each end. Say for example you had an array of three items, and wanted the collection view to endlessly scroll. You’d need to add an additional two items onto the array - a copy of item 3 at the beginning of the array, and a copy of item 1 at the beginning:

That’s a simple operation - assuming you’ve got an array of data called originalArray, this method will create an NSMutableArray called workingArray, and pass an immutable copy of that into the UICollectionView’s data source.

{% codeblock lang:objc) -(void)setupDataForCollectionView {

// Create the original set of data
NSArray *originalArray = @[@"itemOne", @"itemTwo", "itemThree"];

// Grab references to the first and last items
// They're typed as id so you don't need to worry about what kind
// of objects the originalArray is holding
id firstItem = originalArray[0];
id lastItem = [originalArray lastObject];

NSMutableArray *workingArray = [originalArray mutableCopy];

// Add the copy of the last item to the beginning
[workingArray insertObject:lastItem atIndex:0];

// Add the copy of the first item to the end
[workingArray addObject:firstItem];

// Update the collection view's data source property
self.dataArray = [NSArray arrayWithArray:workingArray];

} {% endcodeblock)

The new self.dataArray will now contain:

{% codeblock lang:objc) @“itemThree”, @“itemOne”, @“itemTwo”, “itemThree”, @“itemOne” {% endcodeblock)

Creating the effect

The process relies on the ability to move a UICollectionView to a specific indexPath without animation - that’s possible with the scrollToIndexPath:atScrollPosition:animated: method.

This takes three parameters:

  • the indexPath that the collection view should scroll to
  • a UICollectionViewScrollPosition to control where the collection view should scroll to
  • a BOOL to control whether the scrolling should be animated or not

The UICollectionViewScrollPosition parameter controls exactly where the collection view should be scrolled to. Assuming that the collection view is paged, you’d want it to scroll to UICollectionViewScrollPositionLeft which will align the item being displayed with the left-hand edge of the UICollectionView’s frame. If you were working with a vertically-scrolling collection view, you’d want the UICollectionViewScrollPositionTop value.

Here’s how that works in practice when scrolling to the end of the collection view:

Scrolling to the beginning of the collection view is very similar, but works in the opposite direction:

Implementing the process

The key to making this happen is being able to detect when the user has scrolled the collection view to the end, and using that to trigger the scrollToIndexPath:atScrollPosition:animated: method.

To do this, you can exploit the fact that UICollectionView inherits from UIScrollView and so calls the scrollViewDidEndDecelerating: method from the UIScrollViewDelegate protocol when the scrolling stops.

By checking the contentOffset property, you can check where the collectionView has landed, and react accordingly. In this method, the local variable scrollview is a reference to the UICollectionView that’s calling it:

{% codeblock lang:objc)

-(void)scrollViewDidEndDecelerating:(UIScrollview *)scrollview {

// Calculate where the collection view should be at the right-hand end item
float contentOffsetWhenFullyScrolledRight = self.collectionView.frame.size.width * ([self.dataArray count] -1);

if (scrollView.contentOffset.x == contentOffsetWhenFullyScrolledRight) {

    // user is scrolling to the right from the last item to the 'fake' item 1.
    // reposition offset to show the 'real' item 1 at the left-hand end of the collection view

    NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:1 inSection:0];

    [self.collectionView scrollToItemAtIndexPath:newIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];

} else if (scrollView.contentOffset.x == 0)  {

    // user is scrolling to the left from the first item to the fake 'item N'.
    // reposition offset to show the 'real' item N at the right end end of the collection view

    NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:([self.dataArray count] -2) inSection:0];

    [self.collectionView scrollToItemAtIndexPath:newIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];

}

} {% endcodeblock)

First, this calculates the content offset that the collection view has when it’s scrolled fully to the right (so the right-hand-end copy of item 1 is showing). The offset is actually calculated relative to the left edge of the collection view, so it’s the collection view’s width multiplied by the number of items in the collection view minus the width of one item.

Then you can check if the collection view has landed there. If it has, you need to scroll it back so that it shows the ‘real’ item 1 - that’s the second item in the data array, so the corresponding indexPath will be item 1, section 0 (remember that index paths are zero-indexed, so the left-most item will have index path [0,0], the second item will have index path [1,0] and so on).

Having created the index path to scroll to, it’s just a case of calling scrollToItemAtIndexPath:atScrollPosition:animated on the collection view - remembering to set the animated property to NO so that you don’t see the collection view scroll.

If the collection view has been scrolled to the left-most item, it’ll be showing the extra copy of item 3 that was inserted at the start of the workingArray - and the content offset will be 0. This time, you need to scroll the content view to the last-but-one item in the workingArray - which is the ‘real’ item 3.

Its index path is [3,0] - in other words, [workingArray count] - 2. It’s minus 2 - firstly because arrays and index paths are zero-indexed, so the index of the last item in an array is the number of items minus 1; and then you need to display the ‘real’ item 3 which is one place to the left from the end of the array.

Summary

Creating a circular UICollectionView isn’t difficult, and has five steps:

  • update the data to pad it with ‘fake’ items at the beginning the end
  • check the content offset of the collection view when it stops scolling
  • move to the second item in the data if the collection view has stopped at the end
  • move to the last-but-one item in the data if the collection view has stopped at the beginning
  • ensure the animation property of the scrollToItemAtIndexPath: method is NO to hide the movement from the user.