こんにちは、サービス開発部の氏です。
主にiOSのクックパッドアプリの開発を担当しています。
UICollectionViewLayout
みなさん使ってますか?
UICollectionView
でレイアウトを組む際、実際触り始めると実装するための選択肢が複数あり、どれが最適なのか悩ましい場面に遭遇する人もいるのではないかと思います。
今回は、自分が業務で触れた際に得た知見について軽くお話したいと思います。
UICollectionVIewLayout とは
UICollectionView
は Cellのサイズや余白等のレイアウトを管理するため、プロパティとして、 UICollectionViewLayout
を所持しています。
この UICollectionViewLayout
に手をいれることによって、レイアウトを好きな形に変更することができます。
レイアウトを組み立てるときの複数の選択肢
実際に UICollectionViewLayout
をいじろうとすると、大きく分けて三つの選択肢が出てきます。
UICollectionViewFlowLayout
を調整する
UICollectionViewDelegateFlowLayout
を実装する
UICollectionViewLayout (Custom)
を作成する
つづけて、各Layoutで出来ること、出来ないことを挙げていきたいと思います。
どんなレイアウトの組み上げ方をすればよいか等、判断に困った際の参考にしていただければ幸いです。
1. UICollectionViewFlowLayout
を調整する
一つ目は UICollectionViewFlowLayout
をそのまま利用する方法です。
InterfaceBuilderで UICollectionView
を設置すると、初期値としてこの UICollectionViewFlowLayout
が設定されています。
UICollectionViewFlowLayout
では、CellやHeader/FooterのSize等がプロパティとして用意されており、それを変更するだけで良い感じに組み上げてくれます。
let flowLayout = UICollectionViewFlowLayout()
let margin: CGFloat = 3.0
flowLayout.itemSize = CGSize(width: 100.0, height: 100.0)
flowLayout.minimumInteritemSpacing = margin
flowLayout.minimumLineSpacing = margin
flowLayout.sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
let collectionViewController = CollectionViewController(collectionViewLayout: flowLayout)
ですが、Cellの大きさを決める itemSize
では、動的な変更が行なえません。
全てのCellを同じ大きさで表示するのであれば、UICollectionViewFlowLayout
を利用すると良いでしょう。
2. UICollectionViewDelegateFlowLayout
を実装する
二つ目は UICollectionViewDelegateFlowLayout
を実装する方法です。
UICollectionViewDelegateFlowLayout
は UICollectionViewDelegate
を継承した Protocolになっており、各種便利メソッドが用意されています。
基本的には、 UICollectionViewFlowLayout
のプロパティと同等なものが準備されています。
extension CollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.row % 3 == 0 {
return CGSize(width: 100.0, height: 100.0)
}
return CGSize(width: 60.0, height: 60.0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return margin
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return margin
}
}
このレイアウトの利点としては、 indexPath
の情報を参照出来るため、動的なCellのサイズ変更が可能です。
ですが、Protocolとして用意されているものでしか変更を行うことが出来ないため、アニメーションを伴う変化には余り適していないと思います。
3. UICollectionViewLayout (Custom)
を作成する
最後は UICollectionViewLayout
を継承した独自レイアウトを作成する手段です。
自由にレイアウトを組める反面、今までに挙げた2通りの様に良しなにレイアウトを組んでもらえません。
CellやSectionなど各要素の配置先を計算する必要があり手間がかかりますが、その分動的なサイズ変更やレイアウト変更を好きなように行うことができます。(自分で書くので当然ですが…)
UICollectionViewLayout
を継承して利用するには、下記の処理を実装する必要があります。
collectionViewContentSize: CGSize
UICollectionView
の contentSize
を返します。
UICollectionView
は、この contentSize
をもとにスクロール量を判断します。
その為、ここでは表示させたい要素に応じた正確な contentSize
を返さないと思った通りの位置までスクロールをしてくれません。
layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
IndexPath
に応じたCellの UICollectionViewLayoutAttributes
を返します。
UICollectionViewLayoutAttributes
は IndexPath
に応じたセルのレイアウト属性です。
この layoutAttributes
にCellのサイズと座標を指定しておくと、指定通りの座標に表示されます。
この中でCellのサイズ計算等を行う場合、時間がかかる処理などがあるとカクつきの原因となります。
よくある方法として、prepare()
でレイアウト情報を先に計算して配列などに用意しておきここではその情報を返すだけとするケースが多いです。
layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
範囲内に含まれる UICollectionReusableView
(cellやsupplementary view)の UICollectionViewLayoutAttributes
の配列を返します。
基本的には、その範囲に含まれる layoutAttributesForItem(at indexPath: IndexPath)
を取得してくる形になるでしょう。
アニメーションについて
レイアウトを作っていると、アニメーションを求められるケースがそれなりにあるかと思います。
レイアウト変更時のアニメーションは下記の様な形でアニメーションを行うことができます。
collectionView.setCollectionViewLayout(newLayout, animated: true)
他には、Cellの生成時や削除時のレイアウト属性を返すメソッドがあり、それを実装することで insertItems
, deleteItems
でもアニメーションをさせる事ができます。
initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath)
finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath)
また、「UICollectionViewController
, UINavigationController
の組み合わせでのみ」という制限がありますが、
UICollectionViewController
の useLayoutToLayoutNavigationTransitions
の値を true
にしてpushさせると UINavigationBar
と連携した遷移が可能です。
let viewController = UICollectionViewController(collectionViewLayout: newLayout)
viewController.useLayoutToLayoutNavigationTransitions = true
navigationController?.pushViewController(viewController, animated: true)
UICollectionViewController
以外への遷移アニメーションは、 UIViewControllerAnimatedTransitioning
を実装してあげると良いでしょう。
まとめ
UICollectionView
のレイアウトを作るに当たって、ほとんどのケースでは UICollectionViewFlowLayout
や UICollectionDelegateFlowLayout
で事足りるかと思います。
独自レイアウトを採用するケースとしては、行ベース、グリッドベース以外のレイアウトが必要なケースや、各要素のレイアウトが頻繁に変化する場合に必要になってきます。(カバーフローのようなものだったり)
UICollectionView
はiOS10から prefetch
や UICollectionViewFlowLayoutAutomaticSize
等の新しい機能も追加され、表現の幅も増えています。
クックパッド内で利用するのはまだ少し先かもしれませんが、ユーザーがより良い体験を提供できるよう常に心がけていきたいですね。