環境: Swift3
コレクションビューで縦横どちらにもスクロールさせることはできるか調べてみた。
デフォルトのレイアウトクラスであるUICollectionViewLayout
では縦横どちらかの方向にしかスクロールできないようだ。
コレクションビューではUICollectionViewLayout
を継承したクラスでレイアウトを管理する。
縦横にセルを並べたフローレイアウト用のUICollectionViewFlowLayout
が標準で用意されている。
StoryBoard上でUICollectionViewを貼り付けた場合、これがデフォルトで使用されるようになっている。
これを使った場合は基本的に縦横どちらかの方向にしかスクロールできない。
まず、UICollectionViewFlowLayout
を継承したカスタムクラスBidirectionalCollectionLayout
を用意する。
やっていることは、最初にすべてのセルを含んだ縦幅、横幅の計算とUICollectionViewLayoutAttributes
(レイアウト属性を管理するオブジェクト)の作成をしてキャッシュする。
func layoutAttributesForElements(in rect: CGRect)
ではrect 内のすべてのセルのレイアウト属性を返す、
func layoutAttributesForItem(at indexPath: IndexPath)
では indexPath で示されるアイテムのレイアウト属性を返す。
import UIKit final class BidirectionalCollectionLayout: UICollectionViewFlowLayout { weak var delegate: UICollectionViewDelegateFlowLayout? private var layoutInfo: [IndexPath : UICollectionViewLayoutAttributes] = [:] private var maxRowsWidth: CGFloat = 0 private var maxColumnHeight: CGFloat = 0 private func calcMaxRowsWidth() { guard let collectionView = self.collectionView, let delegate = self.delegate else { return } var maxRowWidth: CGFloat = 0 for section in 0..<collectionView.numberOfSections { var maxWidth: CGFloat = 0 for item in 0..<collectionView.numberOfItems(inSection: section) { let indexPath = IndexPath(item: item, section: section) let itemSize = delegate.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath) maxWidth += itemSize.width } maxRowWidth = maxWidth > maxRowWidth ? maxWidth : maxRowWidth } self.maxRowsWidth = maxRowWidth } private func calcMaxColumnHeight() { guard let collectionView = self.collectionView, let delegate = self.delegate else { return } var maxHeight: CGFloat = 0 for section in 0..<collectionView.numberOfSections { var maxRowHeight: CGFloat = 0 for item in 0..<collectionView.numberOfItems(inSection: section) { let indexPath = IndexPath(item: item, section: section) let itemSize = delegate.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath) maxRowHeight = itemSize.height > maxRowHeight ? itemSize.height : maxRowHeight } maxHeight += maxRowHeight } self.maxColumnHeight = maxHeight } private func calcCellLayoutInfo() { guard let collectionView = self.collectionView, let delegate = self.delegate else { return } var cellLayoutInfo: [IndexPath : UICollectionViewLayoutAttributes] = [:] var originY: CGFloat = 0 for section in 0..<collectionView.numberOfSections { var height: CGFloat = 0 var originX: CGFloat = 0 for item in 0..<collectionView.numberOfItems(inSection: section) { let indexPath = IndexPath(item: item, section: section) let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) let itemSize = delegate.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath) itemAttributes.frame = CGRect(x: originX, y: originY, width: itemSize.width, height: itemSize.height) cellLayoutInfo[indexPath] = itemAttributes originX += itemSize.width height = height > itemSize.height ? height : itemSize.height } originY += height } self.layoutInfo = cellLayoutInfo } override func prepare() { self.delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout self.calcMaxRowsWidth() self.calcMaxColumnHeight() self.calcCellLayoutInfo() } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var allAttributes: [UICollectionViewLayoutAttributes] = [] for attributes in self.layoutInfo.values { 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 { return CGSize(width: self.maxRowsWidth, height: self.maxColumnHeight) } }
続いてViewController
の実装。
横方向のセル数はnumberOfItemsInSection
で、縦方向のセル数はnumberOfSections
で定義する。
今回は10x10用意した。
import UIKit class ViewController: UIViewController { @IBOutlet weak var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() } } extension ViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 10 } func numberOfSections(in collectionView: UICollectionView) -> Int { return 10 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell let position = "\(indexPath.section) - \(indexPath.row)" cell.titleLabel.text = position if indexPath.section % 2 == 0 { if indexPath.row % 2 == 0 { cell.backgroundColor = UIColor(hex: 0xff6e86) } else { cell.backgroundColor = .white } } else { if indexPath.row % 2 == 0 { cell.backgroundColor = .white } else { cell.backgroundColor = UIColor(hex: 0xff6e86) } } return cell } } extension ViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let position = "\(indexPath.section) - \(indexPath.row)" print("didSelect:", position) } } extension ViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 200, height: 200) } }
StoryBoardから作成したレイアウトクラスを設定する。