【问题标题】:How to implement horizontally infinite scrolling UICollectionView?如何实现水平无限滚动 UICollectionView?
【发布时间】:2016-03-27 13:15:59
【问题描述】:

我想实现水平无限滚动的UICollectionView

【问题讨论】:

标签: ios swift uiscrollview uicollectionview uiscrollviewdelegate


【解决方案1】:

如果你的数据是静态的并且你想要一种循环行为,你可以这样做:

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
}

基本上,您对您的集合视图说您有大量单元格(Int.max 不会是无限的,但可能会起作用),并且您使用 % 运算符访问您的数据源。在我的示例中,我们将得到“item 0”、“item 1”、“item 2”、“item 0”、“item 1”、“item 2”......

我希望这会有所帮助:)

【讨论】:

  • 非常优雅的解决方案!只是一个补充——如果你想让集合从一开始就向左滚动,你可以使用:CollectionView.scrollToItem() 并在初始化后给它一些大的值。
  • 返回 Int.max 会使模拟器崩溃:(
  • @tarrball 是的,但是如果单元格的大小取决于它所显示的数据,并且您在 heightForRowAtIndexPath 或 @987654324 中计算它们的大小/高度,则单元格重用机制只会解决内存问题@ ,这些方法将被调用的次数与您返回的 numberOf .. InSection 一样多。我猜这可能会导致性能问题?即使我做了一些缓存,这里仍然会有数百万次不必要的调用。
  • @tarrball,不!由于UITableViewUICollectionViewUIScrollView 的子类,系统将在初始化滚动视图以确定其contentSize 时计算每个单元格的大小,我尚未在实际应用程序上测试此方法,只是担心我可能会提出性能问题,很高兴知道您已经实现了它并且工作正常。
  • 如果用户从头向左滑动会发生什么?它会走向-1吗?
【解决方案2】:

显然,Manikanta Adimulam 提出了最接近好的解决方案。最干净的解决方案是将最后一个元素添加到数据列表的开头,并将第一个元素添加到最后一个数据列表位置(例如:[4] [0] [1] [2] [3] [4] [ 0]),所以我们在触发最后一个列表项时滚动到第一个数组项,反之亦然。这适用于具有一个可见项目的集合视图:

  1. 子类 UICollectionView。
  2. 重写 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)
        }
    }
    
  3. 覆盖 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)
            }
        }
    }
    
  4. 覆盖 init 方法并计算您的单元格尺寸:

    let layout = self.collectionViewLayout as! UICollectionViewFlowLayout
    cellPadding = layout.minimumInteritemSpacing
    cellWidth = layout.itemSize.width
    

像魅力一样工作! 如果您想通过具有多个可见项的集合视图来实现此效果,请使用发布在here 的解决方案。

【讨论】:

  • 我使用了一个非常相似的解决方案。这适用于使用分页并一次显示一个单元格的集合视图。我还使用 collectionView(sizeForItemAt indexPath) 函数根据设备宽度来调整单元格的大小,并且接受的答案会使应用程序崩溃,因为它为每个单元格设置大小......所以制作一个非常大的集合不会在我的情况下工作。
【解决方案3】:

我在UICollectionView 中实现了无限滚动。使代码在 github 中可用。你可以试一试。它在 swift 3.0 中。

InfiniteScrolling

您可以使用 pod 添加它。用法很简单。只需初始化InfiniteScrollingBehaviour,如下所示。

infiniteScrollingBehaviour = InfiniteScrollingBehaviour(withCollectionView: collectionView, andData: Card.dummyCards, delegate: self)

并实现所需的委托方法以返回配置的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
    }

它将在您的原始数据集中添加适当的前导和尾随边界元素,并将调整 collectionView 的contentOffset

在回调方法中,它会为您提供原始数据集中某个项目的索引。

【讨论】:

    【解决方案4】:

    测试代码

    我通过简单地重复单元x次来实现这一点。如下,

    声明你想要多少个循环

    let x = 50
    

    实现numberOfItems

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return myArray.count*x // large scrolling: lets see who can reach the end :p
    }
    

    添加这个实用函数来计算arrayIndex给定一个indexPath行

    func arrayIndexForRow(_ row : Int)-> Int {
        return row % myArray.count
    }
    

    实现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
    }
    

    添加实用函数以在给定索引处滚动到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)
    }
    

    【讨论】:

    • 这是一个非常好的解决方案。谢谢您的帮助。如果您可以帮助自动滚动,即在 x 秒后集合视图应该滚动到下一个位置,那将更加有益。任何帮助将不胜感激。
    【解决方案5】:

    还暗示您的数据是静态的,并且您的所有 UICollectionView 单元格应该具有相同的大小,我发现 this promising solution

    您可以在github 下载示例项目并自己运行该项目。 ViewController 中创建UICollectionView 的代码非常简单。

    您基本上遵循以下步骤:

    • 在情节提要中创建InfiniteCollectionView
    • 设置infiniteDataSourceinfiniteDelegate
    • 实现创建无限滚动单元格的必要函数

    【讨论】:

    • 您添加的此链接不再有效,请更新或删除它
    • @IraniyaNaynesh,修复了断开的链接。
    • 在这里创建了一个基于 InfiniteCollectionView 的项目:github.com/vineet15889/Restaurant-menu
    【解决方案6】:

    对于那些正在寻找无限和水平滚动的集合视图(其数据源附加到末尾)的用户,请在 scrollViewDidScroll 中附加到您的数据源并在您的集合视图上调用 reloadData()。它将保持滚动偏移量。

    下面的示例代码。我将我的集合视图用于分页日期选择器,当用户靠近右端(倒数第二个)时,我会在其中加载更多页面(整个月):

    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()
    //    }
    }
    

    编辑: - 当您在数据源的开头插入时,这似乎不起作用。

    【讨论】:

      【解决方案7】:

      如果cell.width == collectionView.width,这个解决方案对我有用:

      首先,你需要你的items * 2

      func set(items colors: [UIColor]) {
          items = colors + colors
      }
      

      然后添加这两个计算变量以确定索引:

      var firstCellIndex: Int {
              var targetItem = items.count / 2 + 1
              
              if !isFirstCellSeen {
                  targetItem -= 1
                  isFirstCellSeen = true
              }
              
              return targetItem
          }
          
          var lastCellIndex: Int {
              items.count / 2 - 2
          }
      

      如您所见,firstCellIndex 有一个标志 isFirstCellSeen。 CV第一次出现时需要这个标志,否则会显示items[1]而不是items[0]。所以不要忘记将该标志添加到您的代码中。

      主要逻辑发生在这里:

      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
              )
          }
      }
      

      就是这样。集合视图滚动现在应该是无限的。我喜欢这个解决方案,因为它不需要任何额外的 pod,而且很容易理解:你只需将 cv 项乘以 2,然后在 indexPath == 0indexPath == lastItem 时总是滚动到中间

      【讨论】:

        【解决方案8】:
        1. 要应用此无限循环功能,您应该有正确的 collectionView 布局

        2. 您需要在最后添加数组的第一个元素,并首先添加数组的最后一个元素
          例如:- 数组 = [1,2,3,4]
          呈现数组 = [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)
          }
          

        【讨论】:

        【解决方案9】:

        此处提供的答案有助于实现该功能。但在我看来,它们包含一些可以避免的低级更新(设置内容偏移量、操作数据源......)。如果您仍然不满意并寻找不同的方法,这就是我所做的。

        主要思想是每当您到达最后一个单元格之前的单元格时更新单元格的数量。每次将项目数增加 1 时,都会产生无限滚动的错觉。为此,我们可以利用scrollViewDidEndDecelerating(_ scrollView: UIScrollView) 函数检测用户何时完成滚动,然后更新集合视图中的项目数。这是实现此目的的代码 sn-p:

        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()
            }
        }
        

        优点

        • 直接实施
        • 不使用 Int.max(我个人认为这不是一个好主意)
        • 不使用任意数字(如 50 或其他)
        • 不更改或操纵数据
        • 没有手动更新内容偏移或任何其他滚动视图属性

        缺点

        • 应该启用分页(尽管可以更新逻辑以支持不分页)
        • 需要维护一些属性的引用(当前索引、当前最大计数)
        • 需要在每个滚动端重新加载集合视图(如果可见单元格很少,则没什么大不了的)。如果您在没有缓存的情况下异步加载某些内容,这可能会严重影响您(这是一种不好的做法,数据应该缓存在单元格之外)
        • 如果你想在两个方向上无限滚动就不行了

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-17
          • 2013-10-18
          • 2012-09-29
          • 2014-11-15
          相关资源
          最近更新 更多