I want to implement UICollectionView
that scrolls horizontally and infinitely?
If your data is static and you want a kind of circular behavior, you can do something like this:
var dataSource = ["item 0", "item 1", "item 2"]
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Int.max // instead of returnin dataSource.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let itemToShow = dataSource[indexPath.row % dataSource.count]
let cell = UICollectionViewCell() // setup cell with your item and return
return cell
}
Basically you say to your collection view that you have a huge number of cells (Int.max won't be infinite, but might do the trick), and you access your data source using the % operator. In my example we'll end up with "item 0", "item 1", "item 2", "item 0", "item 1", "item 2" ....
I hope this helps :)
Int.max
crashes the Simulator :( –
Hag heightForRowAtIndexPath
or sizeForItemAtIndexPath
, these method will be called as many times as your numberOf .. InSection
returned. I'm guessing this will probably cause a performance issue ? There will still be millions of unnecessary calls here even if i do some caching. –
Gitlow UITableViews
in UICollectionViewCells
using this method and everything runs very smooth. –
Lunula UITableView
and UICollectionView
is a subclass of UIScrollView
, the system will calculate the size for each cell when initializing the scroll view to determine its contentSize
, i haven't test this approach on a real-world app ,just concerned that i might raise a performance issue , it's also good to know that you've implement this and it's working fine. –
Gitlow indexPath.row
will never be negative. If you somehow have negative indexes, when index % dataSource.count
result is negative you have to add dataSource.count
to keep accessing the data source in the same order –
Hartley Apparently the closest to good solution was proposed by the Manikanta Adimulam. The cleanest solution would be to add the last element at the beginning of the data list, and the first one to the last data list position (ex: [4] [0] [1] [2] [3] [4] [0]), so we scroll to the first array item when we are triggering the last list item and vice versa. This will work for collection views with one visible item:
- Subclass UICollectionView.
Override UICollectionViewDelegate and override the following methods:
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let numberOfCells = items.count let page = Int(scrollView.contentOffset.x) / Int(cellWidth) if page == 0 { // we are within the fake last, so delegate real last currentPage = numberOfCells - 1 } else if page == numberOfCells - 1 { // we are within the fake first, so delegate the real first currentPage = 0 } else { // real page is always fake minus one currentPage = page - 1 } // if you need to know changed position, you can delegate it customDelegate?.pageChanged(currentPage) } public func scrollViewDidScroll(_ scrollView: UIScrollView) { let numberOfCells = items.count if numberOfCells == 1 { return } let regularContentOffset = cellWidth * CGFloat(numberOfCells - 2) if (scrollView.contentOffset.x >= cellWidth * CGFloat(numberOfCells - 1)) { scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x - regularContentOffset, y: 0.0) } else if (scrollView.contentOffset.x < cellWidth) { scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x + regularContentOffset, y: 0.0) } }
Override layoutSubviews() method inside your UICollectionView in order to always to make a correct offset for the first item:
override func layoutSubviews() { super.layoutSubviews() let numberOfCells = items.count if numberOfCells > 1 { if contentOffset.x == 0.0 { contentOffset = CGPoint(x: cellWidth, y: 0.0) } } }
Override init method and calculate your cell dimensions:
let layout = self.collectionViewLayout as! UICollectionViewFlowLayout cellPadding = layout.minimumInteritemSpacing cellWidth = layout.itemSize.width
Works like a charm! If you want to achieve this effect with collection view having multiple visible items, then use solution posted here.
I have implemented infinite scrolling in UICollectionView
. Made the code available in github. You can give it a try. Its in swift 3.0.
You can add it using pod. Usage is pretty simple. Just intialise the InfiniteScrollingBehaviour
as below.
infiniteScrollingBehaviour = InfiniteScrollingBehaviour(withCollectionView: collectionView, andData: Card.dummyCards, delegate: self)
and implement required delegate method to return a configured UICollectionViewCell
. An example implementation will look like:
func configuredCell(forItemAtIndexPath indexPath: IndexPath, originalIndex: Int, andData data: InfiniteScollingData, forInfiniteScrollingBehaviour behaviour: InfiniteScrollingBehaviour) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellID", for: indexPath)
if let collectionCell = cell as? CollectionViewCell,
let card = data as? Card {
collectionCell.titleLabel.text = card.name
}
return cell
}
It will add appropriate leading and trailing boundary elements in your original data set and will adjust collectionView's contentOffset
.
In the callback methods, it will give you index of an item in the original data set.
Tested code
I achieved this by simply repeating cell for x amount of times. As following,
Declare how many loops would you like to have
let x = 50
Implement numberOfItems
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myArray.count*x // large scrolling: lets see who can reach the end :p
}
Add this utility function to calculate arrayIndex
given an indexPath row
func arrayIndexForRow(_ row : Int)-> Int {
return row % myArray.count
}
Implement cellForItem
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myIdentifier", for: indexPath) as! MyCustomCell
let arrayIndex = arrayIndexForRow(indexPath.row)
let modelObject = myArray[arrayIndex]
// configure cell
return cell
}
Add utility function to scroll to middle of collectionView
at given index
func scrollToMiddle(atIndex: Int, animated: Bool = true) {
let middleIndex = atIndex + x*yourArray.count/2
collectionView.scrollToItem(at: IndexPath(item: middleIndex, section: 0), at: .centeredHorizontally, animated: animated)
}
Also implying that your data is static and that all your UICollectionView
cells should have the same size, I found this promising solution.
You could download the example project over at github and run the project yourself. The code in the ViewController that creates the UICollectionView
is pretty straight forward.
You basically follow these steps:
- Create a
InfiniteCollectionView
in Storyboard - Set
infiniteDataSource
andinfiniteDelegate
- Implement the necessary functions that create your infinitely scrolling cells
For those who are looking for infinitely and horizontally scrolling collection views whose data sources are appended to at the end--append to your data source in scrollViewDidScroll
and call reloadData()
on your collection view. It will maintain the scroll offset.
Sample code below. I use my collection view for a paginated date picker, where I load more pages (of entire months) when the user is towards the right end (second to the last):
func scrollViewDidScroll(scrollView: UIScrollView) {
let currentPage = self.customView.collectionView.contentOffset.x / self.customView.collectionView.bounds.size.width
if currentPage > CGFloat(self.months.count - 2) {
let nextMonths = self.generateMonthsFromDate(self.months[self.months.count - 1], forPageDirection: .Next)
self.months.appendContentsOf(nextMonths)
self.customView.collectionView.reloadData()
}
// DOESN'T WORK - adding more months to the left
// if currentPage < 2 {
// let previousMonths = self.generateMonthsFromDate(self.months[0], forPageDirection: .Previous)
// self.months.insertContentsOf(previousMonths, at: 0)
// self.customView.collectionView.reloadData()
// }
}
EDIT: - This doesn't seem to work when you are inserting at the beginning of the data source.
in case the cell.width == collectionView.width
, this solution has worked for me:
first, you need your items * 2
:
func set(items colors: [UIColor]) {
items = colors + colors
}
Then add these two computed variables to determine the indices:
var firstCellIndex: Int {
var targetItem = items.count / 2 + 1
if !isFirstCellSeen {
targetItem -= 1
isFirstCellSeen = true
}
return targetItem
}
var lastCellIndex: Int {
items.count / 2 - 2
}
as you can see, the firstCellIndex
has a flag isFirstCellSeen
. this flag is needed when the CV appears for the first time, otherwise, it will display items[1]
instead of items[0]
. So do not forget to add that flag into your code.
The main logic happens here:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item == 0 {
scroll(to: firstCellIndex)
} else if indexPath.item == items.count - 1 {
scroll(to: lastCellIndex)
}
}
private func scroll(to row: Int) {
DispatchQueue.main.async {
self.collectionView.scrollToItem(
at: IndexPath(row: row, section: 0),
at: .centeredHorizontally,
animated: false
)
}
}
That was it. The collection view scroll should now be infinite. I liked this solution because it does not require any additional pods and is very easy to understand: you just multiply your cv items by 2 and then always scroll to the middle when the indexPath == 0
or indexPath == lastItem
To apply this infinite loop functionality You should have proper collectionView layout
You need to add the first element of the array at last and last element of the array at first
ex:- array = [1,2,3,4]
presenting array = [4,1,2,3,4,1]func infinateLoop(scrollView: UIScrollView) { var index = Int((scrollView.contentOffset.x)/(scrollView.frame.width)) guard currentIndex != index else { return } currentIndex = index if index <= 0 { index = images.count - 1 scrollView.setContentOffset(CGPoint(x: (scrollView.frame.width+60) * CGFloat(images.count), y: 0), animated: false) } else if index >= images.count + 1 { index = 0 scrollView.setContentOffset(CGPoint(x: (scrollView.frame.width), y: 0), animated: false) } else { index -= 1 } pageController.currentPage = index } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { infinateLoop(scrollView: scrollView) } func scrollViewDidScroll(_ scrollView: UIScrollView) { infinateLoop(scrollView: scrollView) }
The answers provided here are good to implement the feature. But in my opinion they contain some low level updates (setting content offset, manipulating the data source ...) which can be avoided. If you're still not satisfied and looking for a different approach here's what I've done.
The main idea is to update the number of cells whenever you reach the cell before the last one. Each time you increase the number of items by 1 so it gives the illusion of infinite scrolling. To do that we can utilize scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
function to detect when the user has finished scrolling, and then update the number of items in the collection view. Here's a code snippet to achieve that:
class InfiniteCarouselView: UICollectionView {
var data: [Any] = []
private var currentIndex: Int?
private var currentMaxItemsCount: Int = 0
// Set up data source and delegate
}
extension InfiniteCarouselView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// Set the current maximum to a number above the maximum count by 1
currentMaxItemsCount = max(((currentIndex ?? 0) + 1), data.count) + 1
return currentMaxItemsCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
let row = indexPath.row % data.count
let item = data[row]
// Setup cell
return cell
}
}
extension InfiniteCarouselView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
// Detect when the collection view has finished scrolling to increase the number of items in the collection view
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Get the current index. Note that the current index calculation will keep changing because the collection view is expanding its content size based on the number of items (currentMaxItemsCount)
currentIndex = Int(scrollView.contentOffset.x/scrollView.contentSize.width * CGFloat(currentMaxItemsCount))
// Reload the collection view to get the new number of items
reloadData()
}
}
Pros
- Straightforward implementation
- No use of Int.max (Which in my own opinion is not a good idea)
- No use of an arbitrary number (Like 50 or something else)
- No change or manipulation of the data
- No manual update of the content offset or any other scroll view attributes
Cons
- Paging should be enabled (Although the logic can be updated to support no paging)
- Need to maintain a reference for some attributes (current index, current maximum count)
- Need to reload the collection view on each scroll end (Not a big deal if the visible cells are minimal). This might affect you drastically if you're loading something asynchronously without caching (Which is a bad practice and data should be cached outside the cells)
- Doesn't work if you want infinite scroll in both directions
© 2022 - 2024 — McMap. All rights reserved.