ios 如何实现水平无限滚动 UICollectionView?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/34396108/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to implement horizontally infinite scrolling UICollectionView?
提问by Abdelrahman
I want to implement UICollectionView
that scrolls horizontally and infinitely?
我想实现UICollectionView
水平和无限滚动?
回答by cicerocamargo
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" ....
基本上你对你的集合视图说你有大量的单元格(Int.max 不会是无限的,但可能会成功),然后你使用 % 运算符访问你的数据源。在我的示例中,我们最终会得到“item 0”、“item 1”、“item 2”、“item 0”、“item 1”、“item 2”……
I hope this helps :)
我希望这有帮助 :)
回答by Bio-Matic
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:
显然,最接近好的解决方案是由Manikanta Adimulam提出的。最干净的解决方案是在数据列表的开头添加最后一个元素,并将第一个元素添加到最后一个数据列表位置(例如:[4] [0] [1] [2] [3] [4] [ 0]),所以当我们触发最后一个列表项时我们滚动到第一个数组项,反之亦然。这适用于具有一个可见项目的集合视图:
- 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
- 子类 UICollectionView。
覆盖 UICollectionViewDelegate 并覆盖以下方法:
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) } }
覆盖 UICollectionView 中的 layoutSubviews() 方法,以便始终为第一项设置正确的偏移量:
override func layoutSubviews() { super.layoutSubviews() let numberOfCells = items.count if numberOfCells > 1 { if contentOffset.x == 0.0 { contentOffset = CGPoint(x: cellWidth, y: 0.0) } } }
覆盖 init 方法并计算您的单元格尺寸:
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.
奇迹般有效!如果您想通过具有多个可见项的集合视图实现此效果,请使用此处发布的解决方案。
回答by Vishal Singh
I have implemented infinite scrolling in UICollectionView
. Made the code available in github. You can give it a try. Its in swift 3.0.
我已经在UICollectionView
. 使代码在 github 中可用。你可以试一试。它在 swift 3.0 中。
You can add it using pod. Usage is pretty simple. Just intialise the InfiniteScrollingBehaviour
as below.
您可以使用 pod 添加它。用法很简单。只需初始化InfiniteScrollingBehaviour
如下。
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:
并实现所需的委托方法以返回配置的UICollectionViewCell
. 一个示例实现将如下所示:
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
.
它将在原始数据集中添加适当的前导和尾随边界元素,并将调整 collectionView 的contentOffset
.
In the callback methods, it will give you index of an item in the original data set.
在回调方法中,它将为您提供原始数据集中项目的索引。
回答by SebastianR
Also implying that your data is static and that all your UICollectionView
cells should have the same size, I found this promising solution.
还暗示您的数据是静态的并且您的所有UICollectionView
单元格都应该具有相同的大小,我发现了这个有前途的解决方案。
You could download the example project over at githuband run the project yourself. The code in the ViewController that creates the UICollectionView
is pretty straight forward.
您可以在github 上下载示例项目并自己运行该项目。ViewController 中创建 的代码UICollectionView
非常简单。
You basically follow these steps:
您基本上遵循以下步骤:
- Create a
InfiniteCollectionView
in Storyboard - Set
infiniteDataSource
andinfiniteDelegate
- Implement the necessary functions that create your infinitely scrolling cells
InfiniteCollectionView
在故事板中创建一个- 设置
infiniteDataSource
和infiniteDelegate
- 实现创建无限滚动单元格的必要功能
回答by Jo?e Ws
Tested code
测试代码
I achieved this by simply repeatingcell for x amount of times. As following,
我通过简单地重复单元格 x 次来实现这一点。如下,
Declare how many loops would you like to have
声明你想要多少个循环
let x = 50
Implement numberOfItems
实施 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
添加此实用程序函数以计算arrayIndex
给定的 indexPath 行
func arrayIndexForRow(_ row : Int) {
return row % myArray.count
}
Implement cellForItem
实施 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
添加实用函数以滚动到collectionView
给定索引的中间
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)
}
回答by Matthew Quiros
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.
对于那些正在寻找其数据源附加到最后的无限和水平滚动集合视图的人 - 附加到您的数据源scrollViewDidScroll
并调用reloadData()
您的集合视图。它将保持滚动偏移。
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.
编辑: - 当您在数据源的开头插入时,这似乎不起作用。
回答by Manikanta Adimulam
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) }
要应用此无限循环功能,您应该有适当的 collectionView 布局
您需要在最后添加数组的第一个元素,在第一个添加数组的最后一个元素
:- array = [1,2,3,4]
呈现 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) }
回答by Santina
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:
主要思想是每当您到达最后一个之前的单元格时更新单元格的数量。每次将项目数增加 1 时,就会产生无限滚动的错觉。为此,我们可以利用scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
函数来检测用户何时完成滚动,然后更新集合视图中的项目数。这是实现这一目标的代码片段:
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
- 直接实施
- 不使用 Int.max (在我看来这不是一个好主意)
- 不使用任意数字(如 50 或其他数字)
- 不更改或操纵数据
- 无需手动更新内容偏移量或任何其他滚动视图属性
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
- 应该启用分页(虽然可以更新逻辑以支持无分页)
- 需要维护一些属性的引用(当前索引,当前最大计数)
- 需要在每个滚动端重新加载集合视图(如果可见单元格很小,这没什么大不了的)。如果您在没有缓存的情况下异步加载某些内容,这可能会严重影响您(这是一种不好的做法,数据应该缓存在单元格之外)
- 如果您想在两个方向无限滚动,则不起作用