Preventing “wrapping” of items in UICollectionView

被刻印的时光 ゝ 提交于 2019-12-03 14:05:36

Figured out two ways to do this. Both required a custom layout. The problem is that the default flow layout--and I now know from the Collection View Programming Guide this is partly the definition of a flow layout--generates the cell layout attributes based on the bounds of the superview, and will wrap items in a section to keep them in bounds so that scrolling occurs in only one axis. Will skip the code details, as it isn't hard, and my problem was mainly confusion on what approach to take.

Easy way: use a UIScrollView and subclass 'UICollectionViewFlowLayout'. Embed the UICollectionView in a UIScrollView. Set the contentSize property of the scroll view in viewDiDLoad to match the full size that your collection view will occupy (this will let the default flow layout place items in a single line within a section without wrapping). Subclass UICollectionViewFlowLayout, and set that object as your custom layout for the collection view. In the custom flow layout, override collectionViewContentSize to return the full size of the collection view matrix. With this approach, you'll be using a flow layout, but will be able to scroll in both directions to view un-wrapped sections. The disadvantage is that you still have a flow layout that is pretty limited. Plus, it seems clunky to put a UICollectionView inside an instance of its own superclass just to get the functionality that the collection view by itself should have.

Harder way, but more versatile and elegant: subclass UICollectionViewLayout. I used this tutorial to learn how to implement a complete custom layout. You don't need a UIScrollView here. If you forego the flow layout, subclass UICollectionViewLayout, and set that as the custom layout, you can build out the matrix and get the right behavior from the collection view itself. It's more work because you have to generate all the layout attributes, but you'll be positioned to make the collection view do whatever you want.

In my opinion, Apple should add a property to the default flow layout that suppresses wrapping. Getting a device to display a 2D matrix with intact rows and columns isn't an exotic functionality and it seems like it should be easier to do.

Here is complete matrix customLayout:

import UIKit

class MatrixLayout: UICollectionViewLayout {

    var itemSize: CGSize!
    var interItemSpacingY: CGFloat!
    var interItemSpacingX: CGFloat!
    var layoutInfo: Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>!

    required init?(coder aDecoder: NSCoder) {

        super.init(coder: aDecoder)

        itemSize = CGSizeMake(50.0, 50.0)
        interItemSpacingY = 1.0
        interItemSpacingX = 1.0
    }

    override func prepareLayout() {

        var cellLayoutInfo = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()

        let sectionCount = self.collectionView?.numberOfSections()
        var indexPath = NSIndexPath(forItem: 0, inSection: 0)

        for (var section = 0; section < sectionCount; section += 1)
        {
            let itemCount = self.collectionView?.numberOfItemsInSection(section)

            for (var item = 0; item < itemCount; item += 1)
            {
                indexPath = NSIndexPath(forItem:item, inSection: section)
                let itemAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
                itemAttributes.frame = frameForCellAtIndexPath(indexPath)
                cellLayoutInfo[indexPath] = itemAttributes
            }

            self.layoutInfo = cellLayoutInfo
        }
    }

    func frameForCellAtIndexPath(indexPath: NSIndexPath) -> CGRect
    {
        let row = indexPath.section
        let column = indexPath.item

        let originX = (self.itemSize.width + self.interItemSpacingX) * CGFloat(column)
        let originY = (self.itemSize.height + self.interItemSpacingY) * CGFloat(row)

        return CGRectMake(originX, originY, self.itemSize.width, self.itemSize.height)
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
    {
        var allAttributes = Array<UICollectionViewLayoutAttributes>()

        for (index, attributes) in self.layoutInfo
        {
            if (CGRectIntersectsRect(rect, attributes.frame))
            {
                allAttributes.append(attributes)
            }
        }
        return allAttributes
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        return self.layoutInfo[indexPath]
    }


    override func collectionViewContentSize() -> CGSize {

        let width:CGFloat = (self.itemSize.width + self.interItemSpacingX) * CGFloat((self.collectionView?.numberOfItemsInSection(0))!)
        let height:CGFloat = (self.itemSize.height + self.interItemSpacingY) * CGFloat((self.collectionView?.numberOfSections())!)

        return CGSizeMake(width, height)
    }
}

sections are rows, items are columns

Here is a version of AndrewK's code updated to Swift 4:

import UIKit

class CollectionViewMatrixLayout: UICollectionViewLayout {

  var itemSize: CGSize
  var interItemSpacingY: CGFloat
  var interItemSpacingX: CGFloat
  var layoutInfo: [IndexPath: UICollectionViewLayoutAttributes]

  override init() {
    itemSize = CGSize(width: 50, height: 50)
    interItemSpacingY = 1
    interItemSpacingX = 1
    layoutInfo = [IndexPath: UICollectionViewLayoutAttributes]()

    super.init()
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func prepare() {
    guard let collectionView = self.collectionView else {
      return
    }

    var cellLayoutInfo = [IndexPath: UICollectionViewLayoutAttributes]()
    var indexPath = IndexPath(item: 0, section: 0)

    let sectionCount = collectionView.numberOfSections
    for section in 0..<sectionCount {

      let itemCount = collectionView.numberOfItems(inSection: section)
      for item in 0..<itemCount {
        indexPath = IndexPath(item: item, section: section)
        let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        itemAttributes.frame = frameForCell(at: indexPath)
        cellLayoutInfo[indexPath] = itemAttributes
      }

      self.layoutInfo = cellLayoutInfo
    }
  }

  func frameForCell(at indexPath: IndexPath) -> CGRect {
    let row = indexPath.section
    let column = indexPath.item

    let originX = (itemSize.width + interItemSpacingX) * CGFloat(column)
    let originY = (itemSize.height + interItemSpacingY) * CGFloat(row)

    return CGRect(x: originX, y: originY, width: itemSize.width, height: itemSize.height)
  }

  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
  {
    var allAttributes = Array<UICollectionViewLayoutAttributes>()

    for (_, attributes) in self.layoutInfo {
      if (rect.intersects(attributes.frame)) {
        allAttributes.append(attributes)
      }
    }
    return allAttributes
  }

  override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return self.layoutInfo[indexPath]
  }

  override var collectionViewContentSize: CGSize {
    guard let collectionView = self.collectionView else {
      return .zero
    }

    let sectionCount = collectionView.numberOfSections
    let height = (itemSize.height + interItemSpacingY) * CGFloat(sectionCount)

    let itemCount = Array(0..<sectionCount)
      .map { collectionView.numberOfItems(inSection: $0) }
      .max() ?? 0

    let width  = (itemSize.width + interItemSpacingX) * CGFloat(itemCount)

    return CGSize(width: width, height: height)

  }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!