LoginSignup
1
1

More than 3 years have passed since last update.

【Swift】UICollectionViewCompositionalLayoutでレイアウトを組む

Posted at

はじめに

今回はUICollectionViewCompositionalLayoutを使ってGridとInstagram風のレイアウトを組んでみたいと思います。
ezgif.com-gif-maker.gif

GitHub

以下のCollectionCompositeLayoutフォルダに今回のプロジェクトはあります。

カスタムセル実装

表示させるカスタムセルの背景色は以下のように設定しました。

CustomCollectionViewCell
import UIKit

final class CustomCollectionViewCell: UICollectionViewCell {

    static var identifier: String {
        return String(describing: self)
    }

    private let colors: [UIColor] = [.red,
                                     .blue,
                                     .green,
                                     .yellow,
                                     .orange,
                                     .cyan,
                                     .magenta
    ]

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = colors.randomElement()

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Gridレイアウト実装

まず、Gridの方から作ってみたいと思います。

GridViewController
import UIKit

final class GridViewController: UIViewController {

    @IBOutlet private weak var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
            section: gridSection()
        )
        collectionView.register(CustomCollectionViewCell.self,
                                forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.dataSource = self

    }

    private func gridSection() -> NSCollectionLayoutSection {

        let itemCount = 3
        let lineCount = itemCount - 1
        let itemSpacing = CGFloat(2)
        let itemLength = (self.view.frame.size.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)
        //一つのitem
        let item = NSCollectionLayoutItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(itemLength),
                heightDimension: .absolute(itemLength)
            )
        )
        //itemを三つ横並びに
        //.fractionalは親Viewとの割合
        let items = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1),
                heightDimension: .fractionalHeight(1)
            ),
            subitem: item,
            count: itemCount
        )
        //item間のスペース
        items.interItemSpacing = .fixed(itemSpacing)
        //itemsが縦に並ぶグループを作成
        let groups = NSCollectionLayoutGroup.vertical(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1),
                heightDimension: .absolute(itemLength)
            ),
            subitems: [items]
        )
        //groupsからsectionを生成
        let section = NSCollectionLayoutSection(group: groups)
        section.interGroupSpacing = itemSpacing
        return section

    }

}

extension GridViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 200
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier,
                                                      for: indexPath) as! CustomCollectionViewCell
        return cell
    }

}

Gridレイアウト解説

大切なところのみ解説したいと思います。
基本的な構成は以下の画像のようになっていて、Itemを決めてGroupを決めてSectionを決めてLayoutを設定すると言う流れになります。
スクリーンショット 2021-04-11 22.12.28.png

それぞれ使う定数を事前に定義しておきます。

let itemCount = 3
let lineCount = itemCount - 1
let itemSpacing = CGFloat(2)
let itemLength = (self.view.frame.size.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)

一つのアイテムのサイズを決めます。
absoluteとは絶対値と言う意味で、決まった値を幅にする設定にしています。

let item = NSCollectionLayoutItem(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(itemLength),
        heightDimension: .absolute(itemLength)
    )
)

次に、アイテムが三つ横並びになったグループを一つ作ります。
横並びのグループを作りたいので、horizontalを指定しています。
fractionalWidthfractionalHeightとは親Viewにたいしてどれぐらいの割合で幅を決めるかと言う意味です。親ビューと同じにしたいのであれば、1(100%)にします。70%にしたい場合は.fractionalWidth(7/10).fractionalWidth(0.7)のようにします。
subItemは先ほど作成したItemdです。それをitemCount(=3)個並べていると言う意味です。

let items = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .fractionalHeight(1)
    ),
    subitem: item,
    count: itemCount
)

これは先ほど作ったグループ(=items)にitemSpacing(=2)だけスペースを入れる設定をしています。

items.interItemSpacing = .fixed(itemSpacing)

そして先ほど作成したグループ(=items)を縦に並べる設定をします。
縦に並べたいので、verticalにします。

let groups = NSCollectionLayoutGroup.vertical(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .absolute(itemLength)
    ),
    subitems: [items]
)

groupsからsectionを作成し、スペースを開けます。

let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section

そして、最後にこの関数(func gridSection() -> NSCollectionLayoutSectionf)をcollectionViewに設定します。

collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
    section: gridSection()
)

Instagram風レイアウト実装

次に、Instagram風レイアウトを組んでみたいと思います。先ほどのGridレイアウトより少し難しくなりますが、基本は同じです。

final class InstagramViewController: UIViewController {

    @IBOutlet private weak var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
            section: instagramSection()
        )
        collectionView.register(CustomCollectionViewCell.self,
                                forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.dataSource = self

    }

    private func instagramSection() -> NSCollectionLayoutSection {

        let itemSpacing = CGFloat(2)
        let smallItemLength = (self.view.frame.size.width - (itemSpacing * 2)) / 3
        let largeItemLength = smallItemLength * 2 + itemSpacing

        // 小itemが縦に2つ並んだグループ
        let smallItem = NSCollectionLayoutItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(smallItemLength),
                heightDimension: .absolute(smallItemLength)
            )
        )
        let smallItemVerticalGroup = NSCollectionLayoutGroup.vertical(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(smallItemLength),
                heightDimension: .absolute(largeItemLength)
            ),
            subitem: smallItem,
            count: 2
        )
        smallItemVerticalGroup.interItemSpacing = .fixed(itemSpacing)

        // 小itemが縦に2つ並んだグループを横に3つ並べたグループ
        let smallItemsGroup = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(largeItemLength)
            ),
            subitem: smallItemVerticalGroup,
            count: 3
        )
        smallItemsGroup.interItemSpacing = .fixed(itemSpacing)

        // 大item + 小item*2 のグループ
        let largeItem = NSCollectionLayoutItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(largeItemLength),
                heightDimension: .absolute(largeItemLength)
            )
        )

        let largeItemLeftGroup = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(largeItemLength)
            ),
            subitems: [largeItem, smallItemVerticalGroup]
        )
        largeItemLeftGroup.interItemSpacing = .fixed(itemSpacing)

        let largeItemRightGroup = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(largeItemLength)
            ),
            subitems: [smallItemVerticalGroup, largeItem]
        )
        largeItemRightGroup.interItemSpacing = .fixed(itemSpacing)

        // 各グループを縦に並べたグループ
        let subitems = [largeItemLeftGroup, smallItemsGroup, largeItemRightGroup, smallItemsGroup]
        let heightDimension = NSCollectionLayoutDimension.absolute(
            //sectionの間隔は50
            largeItemLength * CGFloat(subitems.count) + (itemSpacing * 50)
        )

        // 高さの計算は後に追加するスペース分も足す
        let groups = NSCollectionLayoutGroup.vertical(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: heightDimension),
            subitems: subitems
        )
        groups.interItemSpacing = .fixed(itemSpacing)
        let section = NSCollectionLayoutSection(group: groups)
        section.interGroupSpacing = itemSpacing
        return section

    }

}

extension InstagramViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 200
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier,
                                                      for: indexPath) as! CustomCollectionViewCell
        return cell
    }

}

Instagram風レイアウト解説

こちらも大切のところのみ解説します。

今回はitemのサイズが異なるのでそれぞれ設定します。

let itemSpacing = CGFloat(2)
let smallItemLength = (self.view.frame.size.width - (itemSpacing * 2)) / 3
let largeItemLength = smallItemLength * 2 + itemSpacing

小さいアイテムが縦に2つ並んだグループを作成しています。

let smallItem = NSCollectionLayoutItem(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(smallItemLength),
        heightDimension: .absolute(smallItemLength)
    )
)
let smallItemVerticalGroup = NSCollectionLayoutGroup.vertical(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(smallItemLength),
        heightDimension: .absolute(largeItemLength)
    ),
    subitem: smallItem,
    count: 2
)
smallItemVerticalGroup.interItemSpacing = .fixed(itemSpacing)

そして、そのグループを横に三つ並べたグループを作ります。

let smallItemsGroup = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .absolute(largeItemLength)
    ),
    subitem: smallItemVerticalGroup,
    count: 3
)
smallItemsGroup.interItemSpacing = .fixed(itemSpacing)

次に、左側に大きいアイテムと小さいアイテムの組み合わせ、右側に大きいアイテムと小さいアイテムの組み合わせのグループを作成しています。

let largeItem = NSCollectionLayoutItem(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(largeItemLength),
        heightDimension: .absolute(largeItemLength)
    )
)

let largeItemLeftGroup = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .absolute(largeItemLength)
    ),
    subitems: [largeItem, smallItemVerticalGroup]
)
largeItemLeftGroup.interItemSpacing = .fixed(itemSpacing)

let largeItemRightGroup = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .absolute(largeItemLength)
    ),
    subitems: [smallItemVerticalGroup, largeItem]
)
largeItemRightGroup.interItemSpacing = .fixed(itemSpacing)

そして、作ったグループ三つを縦に並べたグループを作成します。

let subitems = [largeItemLeftGroup, smallItemsGroup, largeItemRightGroup, smallItemsGroup]
let heightDimension = NSCollectionLayoutDimension.absolute(
    //sectionの間隔は50
    largeItemLength * CGFloat(subitems.count) + (itemSpacing * 50)
)
let groups = NSCollectionLayoutGroup.vertical(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: heightDimension),
    subitems: subitems
)
groups.interItemSpacing = .fixed(itemSpacing)

最後に、このグループを一つのセクションとして返します。

let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section

Gridの時と同じように、collectionViewに設定すればInstagram風のレイアウト完成です。

collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
    section: instagramSection()
)

おわりに

UITableViewがいらない日が来るかもしれませんね。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1