Compositional Layoutsで複雑なUIのCollectionViewを実装する
WWDC2019で発表されたCompositional Layoutsは、どうやら複雑なレイアウトのUICollectionViewを簡単に実現することができるらしいという事で、調査と実装をしてみました。
今回は、Compositional Layoutsを利用して、インスタグラムやWearで使われているようなUIを実現。割とサクッと実現できました。
サンプルコードはこちらに載せています。
Compositional Layoutsざっくり説明
そもそもCompositional Layoutsとはどういうものなのかを簡単にまとめます。(※WWDC2019の動画やデモコードを調べた上での、私なりの理解なので間違っていたらご指摘いただきたいです🙏)
まず、Compositional LayoutsはItem、Group、Section の3つのComponentで構成されているようです。
そして、それらは階層構造になっており、図にすると以下のような感じ
- Item
- レイアウトを構成する一番小さな部品で、Cellやヘッダー、フッターになるもの
- Group
- Itemを一つにまとめて構成するレイアウトの基本単位
- Section
- Groupをまとめて、それらに対してInsetsなどを追加できる
- Layout
- 最終的にレンダリングされる単位
実際にこのLayoutをCollectionViewに適用するコードは簡単で、UICollectionView
の初期化時に生成したレイアウトを渡すだけです。
こんな感じ👇
var collectionView: UICollectionView! = nil override func viewDidLoad() { super.viewDidLoad() collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) ... } func createLayout() -> UICollectionViewLayout { //ここでUICollectionViewCompositionalLayoutを作成し返却 let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in //各レイアウトの定義 } return layout }
複雑なUIを実現する
上記を踏まえて、サンプルUIの実現方法について説明します。 今回の構成ですが、3つのItemを持つGroupを、4つ組み合わせて1つのSectionにしています。
図で説明すると以下のような構成です。4つのGroupでSectionを構成しています。
各Groupの構成は以下の通りです。
TypeA・TypeB
サイズの違うItemを扱うため、Groupの中も2つのGroupで構成するようにしています。 2つの小さなItemで構成されるGroupと、1つのItemで構成されるGroupを1つのGroupにまとめています(言葉にすると分かりにくい...)。
TypeC
こちらは、同じサイズの3つのItemを一つのGroupにまとめています。
実装
コードにするとちょっと長いですが、以下の通りです。
CollectionViewも含む全体のコードは、こちらで公開しています。
func createLayout() -> UICollectionViewLayout { let sideInset: CGFloat = 18 let insideInset: CGFloat = 8 let topInset: CGFloat = 8 let viewWidth: CGFloat = view.bounds.width let smallSquareWidth: CGFloat = (viewWidth - (sideInset * 2 + insideInset * 2)) / 3 let mediumSquareWidth: CGFloat = smallSquareWidth * 2 + insideInset let nestedGroupHeight: CGFloat = mediumSquareWidth + topInset let smallSquareGroupHeight: CGFloat = smallSquareWidth + topInset let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in let nestedGroupTypeA: NSCollectionLayoutGroup = { let smallSquareItem = NSCollectionLayoutItem( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(smallSquareWidth + insideInset))) smallSquareItem.contentInsets = NSDirectionalEdgeInsets(top: topInset, leading: 0, bottom: 0, trailing: insideInset) let smallSquareGroup = NSCollectionLayoutGroup.vertical( layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(smallSquareWidth + insideInset), heightDimension: .fractionalHeight(1.0)), subitem: smallSquareItem, count: 2) let mediumSquareItem = NSCollectionLayoutItem( layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(mediumSquareWidth), heightDimension: .fractionalHeight(1.0))) mediumSquareItem.contentInsets = NSDirectionalEdgeInsets(top: topInset, leading: 0, bottom: 0, trailing: 0) let nestedGroup = NSCollectionLayoutGroup.horizontal( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(nestedGroupHeight)), subitems: [smallSquareGroup, mediumSquareItem]) return nestedGroup }() let nestedGroupTypeB: NSCollectionLayoutGroup = { let mediumSquareItem = NSCollectionLayoutItem( layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(mediumSquareWidth + insideInset), heightDimension: .fractionalHeight(1.0))) mediumSquareItem.contentInsets = NSDirectionalEdgeInsets(top: topInset, leading: 0, bottom: 0, trailing: insideInset) let smallSquareItem = NSCollectionLayoutItem( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(smallSquareWidth + insideInset))) smallSquareItem.contentInsets = NSDirectionalEdgeInsets(top: topInset, leading: 0, bottom: 0, trailing: 0) let smallSquareGroup = NSCollectionLayoutGroup.vertical( layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(smallSquareWidth), heightDimension: .fractionalHeight(1.0)), subitem: smallSquareItem, count: 2) let nestedGroup = NSCollectionLayoutGroup.horizontal( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(nestedGroupHeight)), subitems: [mediumSquareItem, smallSquareGroup]) return nestedGroup }() let nestedGroupTypeC: NSCollectionLayoutGroup = { let smallSquareItem = NSCollectionLayoutItem( layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(smallSquareWidth), heightDimension: .fractionalHeight(1.0))) let smallSquareGroup = NSCollectionLayoutGroup.horizontal( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(smallSquareGroupHeight)), subitem: smallSquareItem, count: 3) smallSquareGroup.interItemSpacing = .fixed(insideInset) smallSquareGroup.contentInsets = NSDirectionalEdgeInsets(top: topInset, leading: 0, bottom: 0, trailing: 0) return smallSquareGroup }() let group = NSCollectionLayoutGroup.vertical( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(nestedGroupHeight * 2 + smallSquareGroupHeight * 2)), subitems: [nestedGroupTypeA, nestedGroupTypeC, nestedGroupTypeB, nestedGroupTypeC]) let section = NSCollectionLayoutSection(group: group) section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: sideInset, bottom: 0, trailing: sideInset) return section } return layout }
実装は複雑に見えますが、考え方はシンプルです。 複数のItemをGroupにまとめて小さなパーツを作り、そのGroupを更に複数まとめてひとつのSectionを作る事で、複雑なUIを実現しています。
各ItemやGroupのサイズ指定方法については、ひとつ上のComponentの高さや幅に対する割合を指定する方法や、絶対値を指定する方法などがあります。 詳しくはドキュメントに書かれています。
今回は、左右のマージンやitem間のスペースを固定したため、絶対値で指定する箇所が多いですが、UIに合わせて指定方法を決めると良いかと思います。
ちなみに、UICollectionViewの初期化時に createLayout()
を渡していますが、collectionView の invalidatelayout()
が呼ばれると createLayout()
も呼ばれるので、画面回転時には今まで通りinvalidatelayout()
を呼んであげれば再計算してくれます。便利!👏
もっと知りたいという方
WWDC2019の動画で、デモコードの解説をしています。動画と見比べながらデモコードをいじってみると、結構分かりやすくておすすめです!
WWDC2019動画(Advances in Collection View Layout)
https://developer.apple.com/videos/play/wwdc2019/215
便利なCompositional Layouts、積極的に活用していきたい!🎉