10

I'm attempting to make a UICollectionView that will scroll indefinitely. Idea being that when you get to the bottom the data array it starts over.

I'm doing this by returning a larger number for numberOfItemsInSection and then doing a % to get the data out of the array.

This works fine which I understand:

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 500
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! PhotoCell

    let index = indexPath.item % photos.count
    let url = photos[index]
}

My question is, is this the best way to achieve this functionality? I've been looking around endlessly online and can't find any other suggestions on how to do it (while using UICollectionView).

rmaddy
  • 298,130
  • 40
  • 468
  • 517
random
  • 8,428
  • 12
  • 46
  • 81
  • 2
    I tried this last year and most i could achieve had visual glitches caused by redrawing the table, i ended up using a third-party library something like this: https://github.com/pronebird/UIScrollView-InfiniteScroll – Nanoc Dec 03 '15 at 14:42
  • I do this exactly like your code does. It's the simplest solution and it's visually good. – Timur Bernikovich Dec 08 '15 at 08:36

5 Answers5

4

What you have is perfectly fine. Another option is to build a collection that wraps your data source array (photos) and offers looped access to its contents:

struct LoopedCollection<Element>: CollectionType {
    let _base: AnyRandomAccessCollection<Element>

    /// Creates a new LoopedCollection that wraps the given collection.
    init<Base: CollectionType where Base.Index: RandomAccessIndexType, Base.Generator.Element == Element>(_ base: Base, withLength length: Int = Int.max) {
        self._base = AnyRandomAccessCollection(base)
        self.endIndex = length
    }

    /// The midpoint of this LoopedCollection, adjusted to match up with
    /// the start of the base collection.
    var startAlignedMidpoint: Int {
        let mid = endIndex / 2
        return mid - mid % numericCast(_base.count)
    }

    // MARK: CollectionType

    let startIndex: Int = 0
    let endIndex: Int

    subscript(index: Int) -> Element {
        precondition(index >= 0, "Index must not be negative.")
        let adjustedIndex = numericCast(index) % _base.count
        return _base[_base.startIndex.advancedBy(adjustedIndex)]
    }
}

You can declare this looping collection alongside your photos array:

let photos: [NSURL] = ...
lazy var loopedPhotos: LoopedCollection<NSURL> = LoopedCollection(self.photos)

And then you can eventually transition your collection view methods to be more generic on the looped collection, or use the looped collection directly:

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return loopedPhotos.count
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! PhotoCell

    let url = loopedPhotos[index]
}
Nate Cook
  • 87,949
  • 32
  • 210
  • 173
  • Interesting idea with the looped collection. But I guess it still doesn't address how you could provide infinite scroll. Probably need to monitor the scroll and then insert rows when the user gets close to the bottom... – Daniel Galasko Dec 09 '15 at 10:50
  • This is exactly what I was needing. You're awesome! – random Dec 09 '15 at 15:18
  • @DanielGalasko It's more that you'd need to start the collectionview in the "middle" of the looped collection, then reset back to that starting point when the user scrolls too far. This isn't too bad with, for example, a UIPicker, but a collection view is far more complex - you would need to handle a lot of different states. – Nate Cook Dec 09 '15 at 15:41
  • If it's a looped collection wouldn't a modulo operation work? Then all they need to do is keep track of the overall size? – Daniel Galasko Dec 09 '15 at 15:42
  • @NateCook, bro could you please help my problem https://stackoverflow.com/questions/44650058/how-to-display-dynamically-data-from-server-in-collectionviewcell-in-tableviewce?noredirect=1#comment76287331_44650058 ? – May Phyu Jun 20 '17 at 11:29
3

Interesting question! Not a bad approach but the downside is that the size of the scroll bar indicator will be very small.

You could set the number of items to twice the number of actual items, then once the user has scrolled into the second half (and scrolling stopped), re-adjust the data offset and the scroll position, then reload. The user would see no change, but once they went to scroll again the scroller position would seem to have jumped up near the top. I like this since the scroll indicator size will stay reasonable, and the user will actually get some visual feedback that they scrolled past the end and are now repeating.

David H
  • 39,114
  • 12
  • 86
  • 125
  • If I understand correctly, your saying that when I get to the end of the list (and not scrolling) to reset the `contentOffset` effectively moving them back to the top of the list. Correct? – random Dec 01 '15 at 14:34
  • Suppose the height of one data set is 1000. When the user stops scrolling at say 1200, then silently reset the contentOffset to 200 with no animation. The view should be exactly as it was prior, except that when the user scrolls again, he will see the scroll indicator has moved "up". Depending on the size of your table, you can do 2x, 4x, 10x, etc. The user of course can "fool" this algorithm by continuing to swipe up constantly to force the scrollview to the bottom cell, but I suspect this would not be much of a real world problem. – David H Dec 02 '15 at 12:31
  • You may hide scroller knob by the way. – Daniyar Dec 08 '15 at 06:47
  • @DavidH, could you please help my problem https://stackoverflow.com/questions/44650058/how-to-display-dynamically-data-from-server-in-collectionviewcell-in-tableviewce?noredirect=1#comment76287331_44650058 ? – May Phyu Jun 20 '17 at 11:29
2

Your code is the simplest solution. And in most cases it will perfectly fit. If you'd like to implement honest infinity scroll you should create your own layout and cells caching.

Timur Bernikovich
  • 4,733
  • 3
  • 39
  • 53
2

you can find more details here

Source : iosnomad enter image description here

Meghs Dhameliya
  • 2,286
  • 21
  • 25
  • This is an excellent answer and awesome post but @natecooks answer more helps fix my issue. – random Dec 09 '15 at 15:17
  • @MeghsDhameliya, bro could you please help my problem https://stackoverflow.com/questions/44650058/how-to-display-dynamically-data-from-server-in-collectionviewcell-in-tableviewce?noredirect=1#comment76287331_44650058 ? – May Phyu Jun 20 '17 at 11:30
  • @MayPhyu checkout answer and let me know – Meghs Dhameliya Jun 20 '17 at 11:42
  • Yes bro @MeghsDhameliya. Thanks bro. I will try your suggestion. – May Phyu Jun 20 '17 at 11:46
  • @MeghsDhameliya , bro I have another problem . Could you please have a look to that problem https://stackoverflow.com/questions/44667335/how-to-pass-prefs-value-data-from-view-controller-to-inside-table-view-cell-with ? – May Phyu Jun 21 '17 at 07:12
1

What you need is two things:

  1. Monitor current scroll offset and notify when the user is close to the bottom.
  2. When this threshold is triggered, signal the UICollectionView/UITableView that there are more rows, append new rows to your data source.

To do 1 is fairly trivial, we can simply extend UIScrollView:

extension UIScrollView {
    /**
    A convenience method that returns true when the scrollView is near to the bottom        
    - returns: true when the current contentOffset is close enough to the bottom to merit initiating a load for the next page
    */
    func canStartLoadingNextPage() -> Bool {
        //make sure that we have content and the scrollable area is at least larger than the scroll views bounds.
        if contentOffset.y > 0 && contentSize.height > 0 && (contentOffset.y + CGRectGetHeight(bounds))/contentSize.height > 0.7 
            return true
        }
        return false
    }
}

This function will return true when you reach 70% of the current content size but feel free to tweak as needed.

Now in our cellForRowAtIndexPath we could technically call our function to determine whether we can append our dataset.

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("niceReuseIdentifierName", forIndexPath: indexPath)        

    if collectionView.canStartLoadingNextPage() {
        //now we can append our data
        self.loadNextPage()
    }
    
    return cell
}

func loadNextPage() {
    self.collectionView.performBatchUpdates({ () -> Void in
        let nextBatch = self.pictures//or however you get the next batch
        self.pictures.appendContentsOf(nextBatch)
        self.collectionView.reloadSections(NSIndexSet(index: 0))
        }, completion: nil)
}

And Voila, you should now have infinite scroll.

Improvements and Future Proofing

To improve this code you could have an object that can facilitate this preload for any UIScrollView subclass. This would also make it easier to transition over to networking calls and more complicated logic:

class ScrollViewPreloader {
    
    enum View {
        case TableView(tableView: UITableView)
        case CollectionView(collectionView: UICollectionView)
    }

    private (set) var view: View
    private (set) var pictures: [Picture] = []
    
    init(view: View) {
        self.view = view
    }
    
    func loadNextPageIfNeeded() {
        
        func shouldLoadNextPage(scrollView: UIScrollView) -> Bool {
            return scrollView.canStartLoadingNextPage()
        }
        
        switch self.view {
        case .TableView(tableView: let tableView):
            if shouldLoadNextPage(tableView) {
               loadNextPageForTableView(tableView)
            }
            break
        case .CollectionView(collectionView: let collectionView):
            if shouldLoadNextPage(collectionView) {
                loadNextPageForCollectionView(collectionView)
            }
            break
        }
    }
    
    private func loadNextBatchOfPictures() -> [Picture] {
        let nextBatch = self.pictures//or however you get the next batch
        self.pictures.appendContentsOf(nextBatch)
        return self.pictures
    }
    
    private func loadNextPageForTableView(tableView: UITableView) {
        tableView.beginUpdates()
        loadNextBatchOfPictures()
        tableView.reloadData()//or you could call insert functions etc
        tableView.endUpdates()
    }
    
    private func loadNextPageForCollectionView(collectionView: UICollectionView) {
        collectionView.performBatchUpdates({ () -> Void in
            self.loadNextBatchOfPictures()
            collectionView.reloadSections(NSIndexSet(index: 0))
            }, completion: nil)
        
    }
}

struct Picture {
    
}

and you would be able to call it using the loadNextPageIfNeeded() function inside cellForRow

Community
  • 1
  • 1
Daniel Galasko
  • 21,827
  • 7
  • 71
  • 93
  • This is a good answer but the issue is that the memory footprint would consistently grow even after the initial full batch of data is loaded. – random Dec 09 '15 at 15:16
  • are you repeatedly adding `self.pictures.appendContentsOf` batches to `self.pictures` as we continue to scroll down. Once we have loaded all the pictures and added them to `self.pictures` we wouldn't want to keep adding and adding to it. – random Dec 09 '15 at 16:09
  • as a tangent, you're answer here was epic and saved my ass: http://stackoverflow.com/questions/25895311/uicollectionview-self-sizing-cells-with-auto-layout/25896386#25896386 – random Dec 09 '15 at 16:12
  • No @random we only append to the array once we reach 70% of the page. That code snippet mirrors what I use for a production app. The content size increases after we call performBatchUpdates – Daniel Galasko Dec 09 '15 at 16:26
  • @random glad to hear it helped, I can't get self sizing cells working on my Github, have to look into it again when I have time – Daniel Galasko Dec 09 '15 at 16:27
  • 1
    I think this is conflating a looping list with automatic loading of paged data. Both let you scroll on and on forever, but OP is looking for a finite set of data looped back to the top, not a way to scroll through a (practically speaking) infinite set of data. – Nate Cook Dec 09 '15 at 17:37
  • @NateCook seeing as your answer is accepted I agree but the actual question does a lot to indicate it's specific to UICollectionView and the implementation of infinite scroll – Daniel Galasko Dec 09 '15 at 18:30
  • @DanielGalasko, bro could you please help my problem https://stackoverflow.com/questions/44650058/how-to-display-dynamically-data-from-server-in-collectionviewcell-in-tableviewce?noredirect=1#comment76287331_44650058 ? – May Phyu Jun 20 '17 at 11:30