2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CollectionViewのSection背景色を動的に変える方法

Posted at

やりたいこと

サーバーから、カラーコードが返ってきた場合に、 カラーコードをCollectionViewのSection背景色に反映する。
CompositionalLayoutは使用してない。 

調べたこと

DecorationViewは動的に変えるものじゃない

CompositionalLayoutにdecorationItemsがあったから、普通のCollectionViewにもそんな感じの背景色変える方法があるのでは?そもそもdecorationItemsは動的に変えられるものなのか?と疑問に思い調べたけど、動的に変える方法はなさそうだった。ChatGPTにも聞いてみたが、怒られた。
スクリーンショット 2023-12-15 10.09.58.png

UICollectionElementCategoryについて

UICollectionViewLayoutには、以下の要素が重要。

  • UICollectionElementCategory.cell: セル
  • UICollectionElementCategory.supplementaryView: ヘッダやフッタなどの補助的なView
  • UICollectionElementCategory.decorationView: 区切り線や背景などの装飾View

表示する情報を変えたり、UIを提供する場合は.supplementaryViewを使用し、決まったデザインを表示するだけであれば.decorationViewを用いる。

HeaderやFooterのようにsupplementaryViewを登録しておいて、動的に内容を変えられれば良いのかなー

ちなみにDecorationViewを使用する方法については?

  1. UICollectionReusableViewを継承したDecorationViewを作成
class BackgroundDecorationView: UICollectionReusableView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        configureView()
    }

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

    private func configureView() {
        backgroundColor = UIColor.lightGray // ここで背景色を設定
    }
}
  1. UICollectionViewFlowLayoutを継承してカスタムクラス作成
    以下は例なのでこんな感じというだけ
class CustomFlowLayout: UICollectionViewFlowLayout {

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

    private func setupLayout() {
        // デコレーションビューの登録
        register(BackgroundDecorationView.self, forDecorationViewOfKind: "BackgroundDecorationView")
    }

    // rect内にある要素のレイアウト情報を求めるメソッドをオーバーライドしてDecorationViewを作成して返す
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let layoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        var allAttributes = layoutAttributes

        // セクションごとにデコレーションビューのレイアウト属性を追加
        for section in 0..<collectionView!.numberOfSections {
            let indexPath = IndexPath(item: 0, section: section)
            if let decorationAttributes = layoutAttributesForDecorationView(ofKind: "BackgroundDecorationView", at: indexPath) {
                allAttributes.append(decorationAttributes)
            }
        }

        return allAttributes
    }

    override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, with: indexPath)

        // ここでデコレーションビューのサイズや位置を設定
        // ItemCellなどから求める必要ある
        attributes.frame =  xxx
        attributes.zIndex = -1

        return attributes
    }
}

DecorationView毎に背景色を変えたい場合は、UICollectionViewLayoutAttributesを拡張して、UICollectionViewLayoutAttributesをインスタンス化した際に、colorを変更できるようにするらしい。

supplementaryViewを使用して背景色を動的に変えるやり方

1. UICollectionReusableViewでカスタムクラス作成

import UIKit

final class SectionBackgroundView: UICollectionReusableView {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }

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

2. UICollectionViewFlowLayoutを継承してカスタムクラスを作成

import UIKit

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    static let collectionElementKindSectionBackground = "UICollectionElementKindSectionBackground"
    private var sectionBackgroundAttributes: [Int: UICollectionViewLayoutAttributes] = [:]

    override init() {
        super.init()
    }

    override func prepare() {
        super.prepare()
        // prepareで計算を行うことでカクツキにくくする
        prepareSectionAttribute()
    }

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

    private func prepareSectionAttribute() {
        guard let collectionView = self.collectionView else { return }
        self.sectionBackgroundAttributes.removeAll()
        for section in 0..<collectionView.numberOfSections {
            guard let minCell = super.layoutAttributesForItem(at: IndexPath(row: 0, section: section)),
                  let maxCell = super.layoutAttributesForItem(at: IndexPath(row: collectionView.numberOfItems(inSection: section) - 1, section: section)) else {
                return
            }
            let height = maxCell.frame.maxY - minCell.frame.minY + self.sectionInset.bottom
            let attributes = UICollectionViewLayoutAttributes(
                forSupplementaryViewOfKind: CustomCollectionViewFlowLayout.collectionElementKindSectionBackground,
                with: IndexPath(row: 0, section: section))
            attributes.zIndex = -1

            attributes.frame = CGRect(x: 0, y: minCell.frame.minY - self.sectionInset.top, width: collectionView.bounds.width, height: height)
            self.sectionBackgroundAttributes[section] = attributes
        }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard collectionView?.dataSource != nil else { return nil }
        guard var allAttributes =
                super.layoutAttributesForElements(in: rect) else { return nil }
        allAttributes.append(contentsOf: self.sectionBackgroundAttributes.values.filter { $0.frame.intersects(rect) })
        return allAttributes
    }

    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        switch elementKind {
        case CustomCollectionViewFlowLayout.collectionElementKindSectionBackground:
            guard indexPath.item == 0 else { return nil }
            return self.sectionBackgroundAttributes[indexPath.section]
        default:
            return super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)
        }
    }
}
  • layoutAttributesForElementsの中で計算を行うと、カクツキの原因になるそうなので、prepare()で行なっている。
  • Sectionの位置計算については、どこまでを範囲とするかなど違ってくると思うので、臨機応変に対応できるよう修正していく。
  • layoutAttributesForElementsで既存のItemに加えて返す
  • layoutAttributesForSupplementaryViewをオーバーライドしてkindを検証し、当てはまる場合に、保持しておいた配列から値を返すようにする

 

3. CollectionViewへの登録(header, footer同様)

collectionView.register(SectionBackgroundView.self, forSupplementaryViewOfKind: CustomCollectionViewFlowLayout.collectionElementKindSectionBackground, withReuseIdentifier: "SectionBackgroundView")

4. Delegateメソッドの実装

// configure
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
  switch kind {
  case CustomCollectionViewFlowLayout.collectionElementKindSectionBackground: // section background
    return collectionView.dequeueReusableSupplementaryView(ofKind: CustomCollectionViewFlowLayout.collectionElementKindSectionBackground, withReuseIdentifier: "SectionBackgroundView", for: indexPath)
  case foo: // else
    return bar
  }
}

まとめ

他にも方法があったのでより良いやり方は試して行きたいー
てか、Section背景色変えるのこんなめんどくさいのかや!びっくり

参考

  • UICollectionViewLayoutの継承したカスタムクラスでSectionの位置、サイズを計算し、SupplementaryViewとして使えるようにた例

  • 以下のサイト見るとカスタムクラスを作成して新しくレイアウトを作ることに対してイメージつきやすいかも

  • DecorationViewの背景色を変えている例

  • 静的にDecorationViewを設定している例

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?