はじめに
こんにちは。Yossyです。
最近、CollectionViewを利用してアイコンが格子状に並んでいるViewを作成しています。
ただ、2行と3行目の間だけは他の行よりも間隔を大きくして、ボタンを設置します。
そこで、カスタムレイアウト(手作りのレイアウト)を作成してみたので、備忘録を兼ねて作り方を紹介してみたいと思います。
作成するレイアウト
レイアウト
4×3の格子状。2行目と3行目の間にボタン設置用の間隔を空ける。
手順
UICollectionViewFlowLayout
を継承したクラスの中で下記を4つを実装します。
詳細はリンク先を参照して下さい。
prepare( )、layoutAttributesForItem( at:)、layoutAttributesForElements(in:)、collectionViewContentSize
大枠の流れとしては、
①
prepare( )
内で12個分のセルの位置(CGRect)を定義する。
②それを基に
layoutAttributesForItem(at: )
で各セルの
UICollectionViewLayoutAttributes
を生成する。
③
layoutAttributesForElements(in: )
でレイアウト作成に必要な
[UICollectionViewLayoutAttributes]
を返す。
④
CollectionViewContentSize
に値を入力する。
prepare( )
レイアウトの初回作成時と、レイアウト変更時に(invalidateLayout( )が呼ばれる度に)呼ばれます。
ここで、12個分のセルの位置情報(CGRect)を定義します。
具体的な実装としては、セルの位置情報の配列を作って、以後の処理に使用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | override func prepare() { super.prepare() if let collectionView = self.collectionView { contentSize = CGSize(width: collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right, height: 0) // セルの幅 let cellWidth: CGFloat = (contentSize.width - super.sectionInset.left - super.sectionInset.right - (super.minimumInteritemSpacing * (CGFloat(numOfCellsinLine) - 1.0))) / CGFloat(numOfCellsinLine) // セルの高さ let cellHeight: CGFloat = 84 // セルの位置情報用のプロパティ var x:CGFloat = 0 var y:CGFloat = 0 let numberOfCellsInSection = collectionView.numberOfItems(inSection: 0) // セル毎の位置情報の計算 for i in (0..<numberOfCellsInSection) { if (i + 1) % 4 == 1{ x = super.sectionInset.left }else{ x += cellWidth + self.minimumInteritemSpacing } let row = floor( Double(i) / 4.0) if row < 2{ y = (cellHeight + super.sectionInset.top) * CGFloat(row) }else{ y = (cellHeight + super.sectionInset.top) * CGFloat(row) + 56 } let cellRect = CGRect(x: x, y: y, width: cellWidth, height: cellHeight) cellRects.append(cellRect) } } } |
layoutAttributesForItem(at indexPath: IndexPath)
各indexPathに対してのUICollectionViewLayoutAttributesを返却します。
UICollectionViewLayoutAttributesは、セルサイズや位置情報のプロパティを持っています。
具体的な実装としては、prepare( )で作成した配列から、セルサイズと位置情報を含んだUICollectionViewLayoutAttributesを生成しています。
1 2 3 4 5 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = super.layoutAttributesForItem(at: indexPath)?.copy() as! UICollectionViewLayoutAttributes attributes.frame = cellRects[indexPath.row] return attributes } |
layoutAttributesForElements(in rect: CGRect)
引数のrectに含まれる全てのセルやviewのUICollectionViewLayoutAttributesの配列を返却します。
ここで返却されるUICollectionViewLayoutAttributesに従い、各セルのレイアウトが作成されます。
具体的な実装としては、
rect.intersects
で前述のlayoutAttributesForItem(at: )で取得したrect範囲内のUICollectionViewLayoutAttributesの配列を返しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var layoutAttributes:[UICollectionViewLayoutAttributes] = [] let numberOfCellsInSection = collectionView.numberOfItems(inSection: 0) for i in 0..<numberOfCellsInSection { let indexPath = IndexPath(row: i, section: 0) if let attributes = layoutAttributesForItem(at: indexPath) { if (rect.intersects(attributes.frame)) { layoutAttributes.append(attributes) } } } return layoutAttributes } |
CollectionViewContentSize
レイアウト作成時に、カスタムレイアウトのスクロール量を判断するのに使用されます。
カスタムレイアウト全体の大きさを返却してあげる必要があります。
デフォルトではCGSizeZeroとなっているらしいので、オーバーライトで値を設定する必要があります。
1 2 3 4 | override var collectionViewContentSize : CGSize { // 作成するレイアウトの幅と高さ return CGSize(width: self.collectionView.frame.width, height: self.collectionView.frame.height) } |
まとめ
コード全体としては以下の通りとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout { // 1行のセル数 var numOfCellsinLine = 4 private var cellRects: [CGRect] = [] private var contentSize = CGSize.zero required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override init() { super.init() self.sectionInset = UIEdgeInsets(top:10, left:10, bottom:10, right:10) self.minimumLineSpacing = 10 self.minimumInteritemSpacing = 10 } override var collectionViewContentSize : CGSize { // 作成するレイアウトの幅と高さ return CGSize(width: self.collectionView!.frame.width, height: self.collectionView!.frame.height) } override func prepare() { super.prepare() if let collectionView = self.collectionView { contentSize = CGSize(width: collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right, height: 0) // セルの幅 let cellWidth: CGFloat = (contentSize.width - super.sectionInset.left - super.sectionInset.right - (super.minimumInteritemSpacing * (CGFloat(numOfCellsinLine) - 1.0))) / CGFloat(numOfCellsinLine) // セルの高さ let cellHeight: CGFloat = 84 // セルの位置情報用のプロパティ var x:CGFloat = 0 var y:CGFloat = 0 let numberOfCellsInSection = collectionView.numberOfItems(inSection: 0) // セル毎の位置情報の計算 for i in (0..<numberOfCellsInSection) { if (i + 1) % 4 == 1{ x = super.sectionInset.left }else{ x += cellWidth + self.minimumInteritemSpacing } let row = floor( Double(i) / 4.0) if row < 2{ y = (cellHeight + super.sectionInset.top) * CGFloat(row) }else{ y = (cellHeight + super.sectionInset.top) * CGFloat(row) + 56 } let cellRect = CGRect(x: x, y: y, width: cellWidth, height: cellHeight) cellRects.append(cellRect) } } } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = super.layoutAttributesForItem(at: indexPath)?.copy() as! UICollectionViewLayoutAttributes attributes.frame = cellRects[indexPath.row] return attributes } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var layoutAttributes:[UICollectionViewLayoutAttributes] = [] let numberOfCellsInSection = collectionView!.numberOfItems(inSection: 0) for i in 0..<numberOfCellsInSection { let indexPath = IndexPath(row: i, section: 0) if let attributes = layoutAttributesForItem(at: indexPath) { if (rect.intersects(attributes.frame)) { layoutAttributes.append(attributes) } } } return layoutAttributes } } |
最後に、CollectionViewのインスタンスを生成する際の引数に指定します。
1 2 | let customLayout = CustomCollectionViewFlowLayout() collectionView = UICollectionView(frame: CGRect, collectionViewLayout: customLayout) |
さいごに
カスタムレイアウトを上手に使えるようになれば、CollectionViewを使用しての実装の幅が大きく広がると感じました。
今回のカスタムレイアウトはシンプルなものでしたが、機会があればもう少し複雑なものにも挑戦してみたいです。