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
).
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]
}
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.
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.
you can find more details here
Source : iosnomad
What you need is two things:
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With