こんにちは、サービス開発部の氏です。
主に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等がプロパティとして用意されており、それを変更するだけで良い感じに組み上げてくれます。
letflowLayout= UICollectionViewFlowLayout() letmargin: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) letcollectionViewController= CollectionViewController(collectionViewLayout:flowLayout)
ですが、Cellの大きさを決める itemSizeでは、動的な変更が行なえません。
全てのCellを同じ大きさで表示するのであれば、UICollectionViewFlowLayoutを利用すると良いでしょう。
2. UICollectionViewDelegateFlowLayoutを実装する
二つ目は UICollectionViewDelegateFlowLayoutを実装する方法です。
UICollectionViewDelegateFlowLayoutは UICollectionViewDelegateを継承した Protocolになっており、各種便利メソッドが用意されています。
基本的には、 UICollectionViewFlowLayoutのプロパティと同等なものが準備されています。
extensionCollectionViewController:UICollectionViewDelegateFlowLayout { funccollectionView(_ 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) } funccollectionView(_ collectionView:UICollectionView, layout collectionViewLayout:UICollectionViewLayout, insetForSectionAt section:Int) ->UIEdgeInsets { return UIEdgeInsets(top:margin, left:margin, bottom:margin, right:margin) } funccollectionView(_ collectionView:UICollectionView, layout collectionViewLayout:UICollectionViewLayout, minimumLineSpacingForSectionAt section:Int) ->CGFloat { return margin } funccollectionView(_ 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と連携した遷移が可能です。
letviewController= UICollectionViewController(collectionViewLayout:newLayout) viewController.useLayoutToLayoutNavigationTransitions =true navigationController?.pushViewController(viewController, animated:true)
UICollectionViewController以外への遷移アニメーションは、 UIViewControllerAnimatedTransitioningを実装してあげると良いでしょう。
まとめ
UICollectionViewのレイアウトを作るに当たって、ほとんどのケースでは UICollectionViewFlowLayoutや UICollectionDelegateFlowLayoutで事足りるかと思います。
独自レイアウトを採用するケースとしては、行ベース、グリッドベース以外のレイアウトが必要なケースや、各要素のレイアウトが頻繁に変化する場合に必要になってきます。(カバーフローのようなものだったり)
UICollectionViewはiOS10から prefetchや UICollectionViewFlowLayoutAutomaticSize等の新しい機能も追加され、表現の幅も増えています。
クックパッド内で利用するのはまだ少し先かもしれませんが、ユーザーがより良い体験を提供できるよう常に心がけていきたいですね。