【Swift】iOS13のCollectionView、CompositionalLayoutsでモダンにレイアウトする
おはようございます!しょーです!
最近は社内ペアプロをよくやっており、問題解決に非常に役立っています。
一人で悩む時間が減るし気づきが多くて非常に有意義な時間になっている。続けていこう。
今日はCollectionViewの話をしようと思います。
iOS13より使用できる、Composinal Layoutsというプロパティの説明になります。
それでは行ってみよう。
なぜCollectionView?
今までリストを作成するときはUITableViewをしようしてリストを作成していました。
ですがAppleがUICollectionViewを今後推奨していくとのことで、もしかしたら近い未来にTableViewがなくなってしまう未来がきてしまうかもしれません。
なので、今まで逃げてきたCollectionViewを勉強して、今後は採用して行こうと思ったのです。
基本的なCollectionViewの使い方
基本的な使い方はUITableViewと一緒ですが、今までのCollectionViewを使用するとなると
- UICollectionViewDelegate
- UICollectionViewDataSource
- UICollectionViewDelegateFlowLayout
の三つの構成が必要になります。
UICollectionViewDelegate
セルが選択された場合のデリゲートメソッドなど
UICollectionViewDataSource
cellForItemAtなどセルの生成を行うメソッドなど
UICollectionViewDelegateFlowLayout
CollectionViewのレイアウトを決めるメソッドなど
TableViewの場合はdelegateとdatasourceの二つでメインの構成を行っていましたが、これにFlowLayoutというレイアウトを決めるメソッドが必要になってきます。
これにより、単一画面に複数のCollectionViewを配置したい場合、それぞれのCollectionViewを用意しなければいけないし、それぞれにtagを追加してユニークに扱わなければいけないので複数対応が困難でした。
ここでこのCollectionViewの複雑性を改善できるのが、iOS13より使用できるCompositionalLayoutsというプロパティです。
なぜ Compositional Layouts?
CompositionalLayoutsは上記の構造を
- Layout
- Section
- Group
- Item
と分けて構成することで、より簡潔に記述することができます。
構成イメージは以下のような感じ。
複数のCollectionViewを単一画面に実装したい場合、従来の実装方法ではそれぞれのViewに対してCollectionViewを用意してあげて、Sectionを切ったり、tagを付与してユニークにしたりして対応する必要がありました。
ですがCompositionalLayoutsをしようすると、セクションによってそれぞれのCollectionViewの実装を分けられるので、一つのCollectionViewオブジェクトで完結することができます。
他のメリットとしては、Itemサイズの記述を画面に対しての比率で記述することができるので、self-sizingなItemを作成できるのも魅力です。
実装方法は思ったより簡単なので、実際に上記のViewを実装していこう!
CompositionalLayoutsを実装する
CollectionViewを用意する
インスタンスを作成しましょう。
class HomeViewController: UIViewController {
// MARK:- UI Parts
private var collectionView: UICollectionView! = nil
// MARK:- Properties
private lazy var dataSource: UICollectionViewDiffableDataSource<Section, Applince>! = nil
override func viewDidLoad() {
super.viewDidLoad()
initViews()
configureDataSource() // 次パートで作成するメソッド
}
private func initViews() {
view.backgroundColor = .white
// ここでインスタンスを代入、collectionViewLayoutにはこれから作成するCompositionalLayoutのメソッドを入れておく
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: generateLayout())
guard let collectionView = collectionView else { return }
view.addSubview(collectionView)
collectionView.backgroundColor = .white
collectionView.register(...)
}
}
lazy var で作成してもいいかと思いますが、今回は後からオブジェクトを代入します。
collectionViewLayoutにいれるgenerateLayout()というレイアウト生成メソッドの規模が大きいからです、、
layout部分の作成を行う
レイアウトの作成を行います。従来までしようしていたFlowLayoutやUICollectionViewDatasourceは必要ありません。
(Delegateはセルタップの実装などで必要になってきますが)
// 1
private enum Section: CaseIterable {
case applianceHorizontal
case article
}
// use for above ios 13
extension HomeViewController {
// 2
private func generateLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let sectionLayoutKind = Section.allCases[sectionIndex]
switch sectionLayoutKind {
case .applianceHorizontal: return self.generateApplianceHorizontalLayout()
case .article: return self.generateArticleLayout()
}
}
return layout
}
// 3
private func generateApplianceHorizontalLayout() -> NSCollectionLayoutSection {
// appliance item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(2/3))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(2/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
// secction
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
// 4
private func generateArticleLayout() -> NSCollectionLayoutSection {
// full article
let fullArticleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1/4))
let fullArticleItem = NSCollectionLayoutItem(layoutSize: fullArticleItemSize)
// two pair
let pairArticleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/2), heightDimension: .fractionalHeight(1))
let pairArticleItem = NSCollectionLayoutItem(layoutSize: pairArticleItemSize)
let pairGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1/4))
let pairGroup = NSCollectionLayoutGroup.horizontal(layoutSize: pairGroupSize, subitem: pairArticleItem, count: 2)
// combine full + pair by vertical
let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, subitems: [fullArticleItem, pairGroup])
// section
let section = NSCollectionLayoutSection(group: nestedGroup)
return section
}
// 5
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collcetionView, indexPath, appliance in
let sectionType = Section.allCases[indexPath.section]
switch sectionType {
case .applianceHorizontal:
let cell = self.collectionView.dequeueReusableCell(for: indexPath, as: ApplianceHolizontalCell.self)
cell.configure()
return cell
case .article:
let cell = self.collectionView.dequeueReusableCell(for: indexPath, as: ArticleCell.self)
return cell
}
}
let snapshot = snapshotForCurrentState()
dataSource.apply(snapshot, animatingDifferences: false)
}
# 6
private func snapshotForCurrentState() -> NSDiffableDataSourceSnapshot<Section, Applince> {
var snapshot = NSDiffableDataSourceSnapshot<Section, Appiance>()
// Appliance section
snapshot.appendSections([Section.applianceHorizontal])
snapshot.appendItems(appliancesDummy)
// Article section
snapshot.appendSections([Section.article])
snapshot.appendItems(appliancesDummy2)
return snapshot
}
}
1
enumでsectionを切り分けます。ここで切ったセクションが上記イメージでグリーンになっているSectionです。
この数だけ、CollectionViewのセクションを作成できるとイメージしてください。
今回は二つのSectionをLayoutに含めたいので適当に二つ作成します。
- Appliance ( 上部のCollectionView, 横スクロール )
- Article ( 横2ペア、1つ横いっぱい、横2ペア、1つ横いっぱい、、、、の繰り返し )
というレイアウトを作成する想定です。
2
先ほど作成したcollectionViewLayoutに入れていたメソッドです。
ここで、1で作成したenumごとにLayoutを入れていくことにより、各Sectionのレイアウトを作成します。
3
各セクションのレイアウトをそれぞれのメソッド内で作成します。
特徴的なのは、それぞれのサイズ指定方法です。
固定サイズをいれる書き方もありますが、fractionalWidth、fractionalHeightを使用する事により、画面サイズに対しての比率でサイズを決めることができます。(0=0%, 1=100%)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(2/3))
今回のアイテムサイズは
- 横幅は1で画面一杯
- 縦幅は画面に対して2/3
の大きさを指定しています。
groupのレイアウトも、このパートはitemを同じサイズなので同様の比率を指定しています。
そしたら作成したgroupeSizeとitemを用いてgroupを作成するのですが、このパートの条件は
- 横スクロール
- groupに対して一列
なのでhorizontalを使用し、count:1を指定してあげます。
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
そしたらsectionとしてgroupをsectionに追加してあげましょう。
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
横スクロールの場合、ページングしたい場合は上記のプロパティでページング設定を行うことができます。
4
基本的には上記3と書き方は一緒ですが、groupやitemのコンビネーションが増えるので少々書き方が異なります。
この場合、
- item2つが横並びのgroup
- item1つが横いっぱいのgroup
の二つのグループがあり、さらにそれらが縦に連なっているセクションになります。
そのようなレイアウトを実装したい場合は、各group事にレイアウトを作成し、最後に縦にネストしてあげる事により実現します。
let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, subitems: [fullArticleItem, pairGroup])
(下にいくほど動的にitemが増える場合はsubitemsの構成を変えなければいけないのかな??)
5
Datasource、もといCellForRowAtの部分はUICollectionViewDiffableDataSource を使用して作成する事になります。
最初に作成したenum Section事にセルを作成しましょう。
そしたら最後にsnapshotという全体のlayoutを作成するメソッドを呼び出し、dataSourceに当てはめてあげます。
let snapshot = snapshotForCurrentState()
dataSource.apply(snapshot, animatingDifferences: false)
6
var snapshot = NSDiffableDataSourceSnapshot<Section, Applince>()
でsnapshotというインスタンスを作成し、ここに作成したいsectionの数だけ値を追加していく形になります。
// Appliance section
snapshot.appendSections([Section.applianceHorizontal])
snapshot.appendItems(appliancesDummy)
// Article section
snapshot.appendSections([Section.article])
snapshot.appendItems(appliancesDummy2)
return snapshot
.appendItemsには配列で作成したい分だけのデータを入れましょう。
nuberOfとか指定しなくていいから楽ですね!自動でやってくれる!
ではサンプルの構造体、配列を適当に作っていきます。
### データの作成
データの作成で気をつけなければいけないところがあります。
データはHashableに紐づくように作成してください。
struct Applince: Hashable {
let id: Int
let image: UIImage = R.image.im_article_sample()!
static func == (lhs: Applince, rhs: Applince) -> Bool {
return lhs.id == rhs.id
}
}
private let appliancesDummy: [Applince] = [
Applince(id: 0),
Applince(id: 1),
Applince(id: 2),
Applince(id: 3),
Applince(id: 4),
Applince(id: 5)
]
private let appliancesDummy2: [Applince] = [
Applince(id: 10),
Applince(id: 11),
Applince(id: 12),
Applince(id: 13),
Applince(id: 14),
Applince(id: 15)
]
CompositinalLayoutsのデータの取り扱いはhashで行っています。
APIでfetchしてくるデータは大体IDがついていたりすると思いますが、ダミーを使用してサンプルを作成する場合、hashに適合しているid等が一緒の場合、snapshotでセクションを作成するときに前のセクションが上書きされてしまいうまく表示されません。
なので要素はそれぞれ、hashをユニークにするようにしてください。
(私がここでハマりました。ペアプロ してくださったMさんありがとうございます!)
## ビルド
これで以下のように表示されるかと思います。
## 終わりに
どうでしたでしょうか。思ったより記述量が少なくなった上に、size指定もきれいになると思いませんか?
TableViewがオワコンらしいのでサボっていたCollectionViewの勉強を再度始めたんですが大きな収穫があってよかったです。
ただiOS 13以上でしか採用できないので、iOS12を担保している場合はavailableなどで切り分ける必要がありますね。
iOS12以下でCompositionalLayouts-likeに記述ができるライブラリがあるのでついでに紹介します。
https://github.com/kishikawakatsumi/IBPCollectionViewCompositionalLayout
これで記述すれば、少々冗長してしまいますが、同様に記述できますし、バージョンを切る際にバッサリ削れるので楽かなーと思います。
こんな感じですねー!
もし良ければ参考にして実装してみてください!
ではお疲れっす!
じゃーな!
ディスカッション
コメント一覧
まだ、コメントがありません