くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

CompositionalLayoutsを使ってみる

今回はCompositionalLayoutsについてです

CompositionalLayoutsはiOS13から登場したCollectionViewのレイアウト構築方法です

これまではUICollectionViewFlowLayoutクラスを使用していましたが、次のような課題がありました

  • Boilerplate code: ボイラープレートコード(定型的なコードで冗長になりがち)
  • Performance considerations: パフォーマンスへの配慮
  • Supplementary and decoration view challenges: 補足・装飾表示の課題
  • Self-sizing challenges: セルフサイジングの課題

その解決策として登場したのが、CompositionalLayoutsです

CompositionalLayoutsは次のように説明されています

  • Composing small layout groups together:小さなレイアウトグループをまとめて合成
  • Layout groups are line-based:レイアウトグループはラインベース
  • Composition instead of subclassing:サブクラス化ではなくコンポジション

CompositionalLayoutsの構造

CompositionalLayoutsは次のように4つの構成があります

Item -> Group -> Section -> Layoutという階層構造になっています。まずは上記図のような構成を再現するようにします

NSCollectionLayoutItem

Compositional Layoutsの階層構造の最小部品のitemは生成時にNSCollectionLayoutSizeを設定する必要があります。 NSCollectionLayoutSizeというのは大まかにいうとセルのサイズです

NSCollectionLayoutSizeでサイズを指定するには次の設定値があります

  • fractionalWidth、fractionalHeight: 画面サイズに対しての比率の割合を指定
  • absolute: 指定した値にサイズを固定
  • estimated: 値の指定はするものの優先される制約がある場合は値は変化する

次のようにサイズをして、itemは生成します

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                              heightDimension: .fractionalHeight(2/3))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

上記の場合、横幅は画面一杯、縦幅は画面に対して2/3のサイズを指定しています。 absoluteやestimatedは.absolute(44)などのように指定します

NSCollectionLayoutGroup

Groupはレイアウトの基本単位を構成します

以下3つの形で定義することが可能です

  • horizontal(縦方向に並べる)
  • vertical(横方向に並べる)
  • custom(並べる方式をカスタマイズ)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalHeight(2/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

上記の場合にはデータを1つずつ縦方向に並べるという実装になります

NSCollectionLayoutSection

NSCollectionLayoutSectionはCollectionViewにおけるSectionと同じ要素です。 Groupを持ち、CompositionalLayoutsにおけるレイアウトの単位になります

NSCollectionLayoutSectionでは、次のようにGroupをSectionに追加します

let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging

また、section.orthogonalScrollingBehaviorでスクロールする形式を設定することができます。 .pagingで横スクロールを設定します

Layouts

Layoutsはここまでで作成したセクション単位のレイアウトを並べます

UIcollectionViewのcollectionViewLayoutに作成するレイアウトを設定します

enum SectionType: CaseIterable {
    case main
    case sub
 }

collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex,_ -> NSCollectionLayoutSection? in
        let sectionLayoutKind = SectionType.allCases[sectionIndex]
        // sectionによって作成するセクションを変更する
        switch sectionLayoutKind {
        case .main:
            return self.generateApplianceHorizontalLayout()
        case .sub:
            return self.generateArticleLayout()
        }
    }

ここまで説明したものをまとめたソースコードは以下になります

import UIKit

class CompositionalLayoutsCollectionView: UICollectionView {
    enum SectionType: CaseIterable {
        case main
        case sub
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        registerNib()
        setupLayouts()
    }
    
 // セルを登録する処理を行う
    private func registerNib() {
    }
    
    private func setupLayouts() {
        collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex,_ -> NSCollectionLayoutSection? in
            let sectionLayoutKind = SectionType.allCases[sectionIndex]
            switch sectionLayoutKind {
            case .main:
                return self.generateApplianceHorizontalLayout()
            case .sub:
                return self.generateArticleLayout()
            }
        }
    }
}

extension CompositionalLayoutsCollectionView {
    private func generateApplianceHorizontalLayout() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                              heightDimension: .fractionalHeight(2/3))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalHeight(2/3))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging
        
        return section
    }
    
    private func generateArticleLayout() -> NSCollectionLayoutSection {
        let fullArticleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                         heightDimension: .fractionalHeight(1/4))
        let fullArticleItem = NSCollectionLayoutItem(layoutSize: fullArticleItemSize)
        
        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)
        
        let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                     heightDimension: .fractionalHeight(1))
        let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, subitems: [pairGroup, fullArticleItem])
        
        let section = NSCollectionLayoutSection(group: nestedGroup)
        
        return section
    }
}

実際にビルドしてみると次のような画面になります。最初に表示していた作成イメージと構成が一致しているのがわかると思います

実装後の表示画面

アイテム間のスペース

アイテム間のスペースを開けたい場合があると思います。そんなときに設定するプロパティを少し紹介します

interGroupSpacing

interGroupSpacingはSectionに設定するプロパティで、セクション内でのグループ間のスペースを設定します。 上下左右一律に設定した値のスペースができます

let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10

interItemSpacing

interItemSpacingはGroupに設定するプロパティで、グループ内でのアイテム間のスペースを設定します。 上下左右一律に設定した値のスペースができます

let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
group.interItemSpacing = .fixed(10.0)

contentInsets

contentInsetsはGroupやSectionなど限定なしに設定できるプロパティで、スペースを設定します。 上下左右に設定した値のスペースができます

let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10)

最後に

ここまで説明しましたが、まだ完全に説明できているわけではありません。 気がついたら更新しようと思いますが、公式ドキュメントやWWWDCの動画を見たりして補いながら学習できるので、試してください!!

参照

developer.apple.com

muchan611.hatenablog.com

future-architect.github.io

blog.shota-ito.com

qiita.com