はじめに
今回はUICollectionViewCompositionalLayoutを使ってGridとInstagram風のレイアウトを組んでみたいと思います。
GitHub
以下のCollectionCompositeLayoutフォルダに今回のプロジェクトはあります。
カスタムセル実装
表示させるカスタムセルの背景色は以下のように設定しました。
import UIKit
final class CustomCollectionViewCell: UICollectionViewCell {
static var identifier: String {
return String(describing: self)
}
private let colors: [UIColor] = [.red,
.blue,
.green,
.yellow,
.orange,
.cyan,
.magenta
]
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = colors.randomElement()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Gridレイアウト実装
まず、Gridの方から作ってみたいと思います。
import UIKit
final class GridViewController: UIViewController {
@IBOutlet private weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
section: gridSection()
)
collectionView.register(CustomCollectionViewCell.self,
forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
collectionView.dataSource = self
}
private func gridSection() -> NSCollectionLayoutSection {
let itemCount = 3
let lineCount = itemCount - 1
let itemSpacing = CGFloat(2)
let itemLength = (self.view.frame.size.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)
//一つのitem
let item = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(itemLength),
heightDimension: .absolute(itemLength)
)
)
//itemを三つ横並びに
//.fractionalは親Viewとの割合
let items = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
),
subitem: item,
count: itemCount
)
//item間のスペース
items.interItemSpacing = .fixed(itemSpacing)
//itemsが縦に並ぶグループを作成
let groups = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(itemLength)
),
subitems: [items]
)
//groupsからsectionを生成
let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section
}
}
extension GridViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 200
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier,
for: indexPath) as! CustomCollectionViewCell
return cell
}
}
Gridレイアウト解説
大切なところのみ解説したいと思います。
基本的な構成は以下の画像のようになっていて、Itemを決めてGroupを決めてSectionを決めてLayoutを設定すると言う流れになります。
それぞれ使う定数を事前に定義しておきます。
let itemCount = 3
let lineCount = itemCount - 1
let itemSpacing = CGFloat(2)
let itemLength = (self.view.frame.size.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)
一つのアイテムのサイズを決めます。
absolute
とは絶対値と言う意味で、決まった値を幅にする設定にしています。
let item = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(itemLength),
heightDimension: .absolute(itemLength)
)
)
次に、アイテムが三つ横並びになったグループを一つ作ります。
横並びのグループを作りたいので、horizontal
を指定しています。
fractionalWidth
やfractionalHeight
とは親Viewにたいしてどれぐらいの割合で幅を決めるかと言う意味です。親ビューと同じにしたいのであれば、1(100%)にします。70%にしたい場合は.fractionalWidth(7/10)
や.fractionalWidth(0.7)
のようにします。
subItem
は先ほど作成したItemdです。それをitemCount(=3)個並べていると言う意味です。
let items = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
),
subitem: item,
count: itemCount
)
これは先ほど作ったグループ(=items)にitemSpacing(=2)
だけスペースを入れる設定をしています。
items.interItemSpacing = .fixed(itemSpacing)
そして先ほど作成したグループ(=items)を縦に並べる設定をします。
縦に並べたいので、vertical
にします。
let groups = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(itemLength)
),
subitems: [items]
)
groupsからsectionを作成し、スペースを開けます。
let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section
そして、最後にこの関数(func gridSection() -> NSCollectionLayoutSectionf
)をcollectionView
に設定します。
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
section: gridSection()
)
Instagram風レイアウト実装
次に、Instagram風レイアウトを組んでみたいと思います。先ほどのGridレイアウトより少し難しくなりますが、基本は同じです。
final class InstagramViewController: UIViewController {
@IBOutlet private weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
section: instagramSection()
)
collectionView.register(CustomCollectionViewCell.self,
forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
collectionView.dataSource = self
}
private func instagramSection() -> NSCollectionLayoutSection {
let itemSpacing = CGFloat(2)
let smallItemLength = (self.view.frame.size.width - (itemSpacing * 2)) / 3
let largeItemLength = smallItemLength * 2 + itemSpacing
// 小itemが縦に2つ並んだグループ
let smallItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(smallItemLength),
heightDimension: .absolute(smallItemLength)
)
)
let smallItemVerticalGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(smallItemLength),
heightDimension: .absolute(largeItemLength)
),
subitem: smallItem,
count: 2
)
smallItemVerticalGroup.interItemSpacing = .fixed(itemSpacing)
// 小itemが縦に2つ並んだグループを横に3つ並べたグループ
let smallItemsGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(largeItemLength)
),
subitem: smallItemVerticalGroup,
count: 3
)
smallItemsGroup.interItemSpacing = .fixed(itemSpacing)
// 大item + 小item*2 のグループ
let largeItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(largeItemLength),
heightDimension: .absolute(largeItemLength)
)
)
let largeItemLeftGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(largeItemLength)
),
subitems: [largeItem, smallItemVerticalGroup]
)
largeItemLeftGroup.interItemSpacing = .fixed(itemSpacing)
let largeItemRightGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(largeItemLength)
),
subitems: [smallItemVerticalGroup, largeItem]
)
largeItemRightGroup.interItemSpacing = .fixed(itemSpacing)
// 各グループを縦に並べたグループ
let subitems = [largeItemLeftGroup, smallItemsGroup, largeItemRightGroup, smallItemsGroup]
let heightDimension = NSCollectionLayoutDimension.absolute(
//sectionの間隔は50
largeItemLength * CGFloat(subitems.count) + (itemSpacing * 50)
)
// 高さの計算は後に追加するスペース分も足す
let groups = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: heightDimension),
subitems: subitems
)
groups.interItemSpacing = .fixed(itemSpacing)
let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section
}
}
extension InstagramViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 200
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier,
for: indexPath) as! CustomCollectionViewCell
return cell
}
}
Instagram風レイアウト解説
こちらも大切のところのみ解説します。
今回はitemのサイズが異なるのでそれぞれ設定します。
let itemSpacing = CGFloat(2)
let smallItemLength = (self.view.frame.size.width - (itemSpacing * 2)) / 3
let largeItemLength = smallItemLength * 2 + itemSpacing
小さいアイテムが縦に2つ並んだグループを作成しています。
let smallItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(smallItemLength),
heightDimension: .absolute(smallItemLength)
)
)
let smallItemVerticalGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(smallItemLength),
heightDimension: .absolute(largeItemLength)
),
subitem: smallItem,
count: 2
)
smallItemVerticalGroup.interItemSpacing = .fixed(itemSpacing)
そして、そのグループを横に三つ並べたグループを作ります。
let smallItemsGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(largeItemLength)
),
subitem: smallItemVerticalGroup,
count: 3
)
smallItemsGroup.interItemSpacing = .fixed(itemSpacing)
次に、左側に大きいアイテムと小さいアイテムの組み合わせ、右側に大きいアイテムと小さいアイテムの組み合わせのグループを作成しています。
let largeItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(largeItemLength),
heightDimension: .absolute(largeItemLength)
)
)
let largeItemLeftGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(largeItemLength)
),
subitems: [largeItem, smallItemVerticalGroup]
)
largeItemLeftGroup.interItemSpacing = .fixed(itemSpacing)
let largeItemRightGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(largeItemLength)
),
subitems: [smallItemVerticalGroup, largeItem]
)
largeItemRightGroup.interItemSpacing = .fixed(itemSpacing)
そして、作ったグループ三つを縦に並べたグループを作成します。
let subitems = [largeItemLeftGroup, smallItemsGroup, largeItemRightGroup, smallItemsGroup]
let heightDimension = NSCollectionLayoutDimension.absolute(
//sectionの間隔は50
largeItemLength * CGFloat(subitems.count) + (itemSpacing * 50)
)
let groups = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: heightDimension),
subitems: subitems
)
groups.interItemSpacing = .fixed(itemSpacing)
最後に、このグループを一つのセクションとして返します。
let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section
Gridの時と同じように、collectionView
に設定すればInstagram風のレイアウト完成です。
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
section: instagramSection()
)
おわりに
UITableViewがいらない日が来るかもしれませんね。