カテゴリー: iOS

CollectionViewのカスタムレイアウトを作ってみた

はじめに

こんにちは。Yossyです。
最近、CollectionViewを利用してアイコンが格子状に並んでいるViewを作成しています。
ただ、2行と3行目の間だけは他の行よりも間隔を大きくして、ボタンを設置します。
そこで、カスタムレイアウト(手作りのレイアウト)を作成してみたので、備忘録を兼ねて作り方を紹介してみたいと思います。

作成するレイアウト

レイアウト

4×3の格子状。2行目と3行目の間にボタン設置用の間隔を空ける。

手順

UICollectionViewFlowLayoutを継承したクラスの中で下記を4つを実装します。
詳細はリンク先を参照して下さい。
prepare( )layoutAttributesForItem( at:)layoutAttributesForElements(in:)collectionViewContentSize

大枠の流れとしては、
prepare( )内で12個分のセルの位置(CGRect)を定義する。
②それを基にlayoutAttributesForItem(at: )で各セルのUICollectionViewLayoutAttributesを生成する。
layoutAttributesForElements(in: )でレイアウト作成に必要な[UICollectionViewLayoutAttributes]を返す。
CollectionViewContentSizeに値を入力する。

prepare( )

レイアウトの初回作成時と、レイアウト変更時に(invalidateLayout( )が呼ばれる度に)呼ばれます。
ここで、12個分のセルの位置情報(CGRect)を定義します。
具体的な実装としては、セルの位置情報の配列を作って、以後の処理に使用しています。

 override func prepare() {
        super.prepare()
        if let collectionView = self.collectionView {
            contentSize = CGSize(width: collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right, height: 0)
            // セルの幅
            let cellWidth: CGFloat = (contentSize.width - super.sectionInset.left - super.sectionInset.right - (super.minimumInteritemSpacing * (CGFloat(numOfCellsinLine) - 1.0))) / CGFloat(numOfCellsinLine)
            // セルの高さ
            let cellHeight: CGFloat = 84
            // セルの位置情報用のプロパティ
            var x:CGFloat = 0
            var y:CGFloat = 0
            
            let numberOfCellsInSection = collectionView.numberOfItems(inSection: 0)
            
            // セル毎の位置情報の計算
            for i in (0..<numberOfCellsInSection) {
                if (i + 1) % 4 == 1{
                    x = super.sectionInset.left
                }else{
                    x += cellWidth + self.minimumInteritemSpacing
                }
                let row = floor( Double(i) / 4.0)
                if  row < 2{
                    y = (cellHeight + super.sectionInset.top) * CGFloat(row)
                }else{
                    y = (cellHeight + super.sectionInset.top) * CGFloat(row) + 56
                }
                
                let cellRect = CGRect(x: x, y: y, width: cellWidth, height: cellHeight)
                cellRects.append(cellRect)
            }
        }
    }

layoutAttributesForItem(at indexPath: IndexPath)

各indexPathに対してのUICollectionViewLayoutAttributesを返却します。
UICollectionViewLayoutAttributesは、セルサイズや位置情報のプロパティを持っています。
具体的な実装としては、prepare( )で作成した配列から、セルサイズと位置情報を含んだUICollectionViewLayoutAttributesを生成しています。

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItem(at: indexPath)?.copy() as! UICollectionViewLayoutAttributes
        attributes.frame = cellRects[indexPath.row]
        return attributes
    }

layoutAttributesForElements(in rect: CGRect)

引数のrectに含まれる全てのセルやviewのUICollectionViewLayoutAttributesの配列を返却します。
ここで返却されるUICollectionViewLayoutAttributesに従い、各セルのレイアウトが作成されます。
具体的な実装としては、rect.intersectsで前述のlayoutAttributesForItem(at: )で取得したrect範囲内のUICollectionViewLayoutAttributesの配列を返しています。

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var layoutAttributes:[UICollectionViewLayoutAttributes] = []
        
        let numberOfCellsInSection = collectionView.numberOfItems(inSection: 0)
        for i in 0..<numberOfCellsInSection {
            let indexPath = IndexPath(row: i, section: 0)
            if let attributes = layoutAttributesForItem(at: indexPath) {
                if (rect.intersects(attributes.frame)) {
                    layoutAttributes.append(attributes)
                }
            }
        }
        return layoutAttributes
    }

CollectionViewContentSize

レイアウト作成時に、カスタムレイアウトのスクロール量を判断するのに使用されます。
カスタムレイアウト全体の大きさを返却してあげる必要があります。
デフォルトではCGSizeZeroとなっているらしいので、オーバーライトで値を設定する必要があります。

override var collectionViewContentSize : CGSize {
        // 作成するレイアウトの幅と高さ
        return CGSize(width: self.collectionView.frame.width, height: self.collectionView.frame.height)
    }

まとめ

コード全体としては以下の通りとなります。

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    
     // 1行のセル数
    var numOfCellsinLine = 4
    private var cellRects: [CGRect] = []
    private var contentSize = CGSize.zero
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init() {
        super.init()
        self.sectionInset = UIEdgeInsets(top:10, left:10, bottom:10, right:10)
        self.minimumLineSpacing = 10
        self.minimumInteritemSpacing = 10
    }
    
    
    override var collectionViewContentSize : CGSize {
        // 作成するレイアウトの幅と高さ
        return CGSize(width: self.collectionView!.frame.width, height: self.collectionView!.frame.height)
    }
    
    
    override func prepare() {
        super.prepare()
        if let collectionView = self.collectionView {
            contentSize = CGSize(width: collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right, height: 0)
            // セルの幅
            let cellWidth: CGFloat = (contentSize.width - super.sectionInset.left - super.sectionInset.right - (super.minimumInteritemSpacing * (CGFloat(numOfCellsinLine) - 1.0))) / CGFloat(numOfCellsinLine)
            // セルの高さ
            let cellHeight: CGFloat = 84
            // セルの位置情報用のプロパティ
            var x:CGFloat = 0
            var y:CGFloat = 0
            
            let numberOfCellsInSection = collectionView.numberOfItems(inSection: 0)
            
            // セル毎の位置情報の計算
            for i in (0.. UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItem(at: indexPath)?.copy() as! UICollectionViewLayoutAttributes
        attributes.frame = cellRects[indexPath.row]
        return attributes
    }
    
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var layoutAttributes:[UICollectionViewLayoutAttributes] = []
        
        let numberOfCellsInSection = collectionView!.numberOfItems(inSection: 0)
        for i in 0..

最後に、CollectionViewのインスタンスを生成する際の引数に指定します。

let customLayout = CustomCollectionViewFlowLayout()
collectionView = UICollectionView(frame: CGRect, collectionViewLayout: customLayout)

さいごに

カスタムレイアウトを上手に使えるようになれば、CollectionViewを使用しての実装の幅が大きく広がると感じました。
今回のカスタムレイアウトはシンプルなものでしたが、機会があればもう少し複雑なものにも挑戦してみたいです。

おすすめ書籍

Yossy

シェア
執筆者:
Yossy
タグ: Swift

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前