環境: 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から作成したレイアウトクラスを設定する。