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

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

## 調べたこと
### DecorationViewは動的に変えるものじゃない
CompositionalLayoutにdecorationItemsがあったから、普通のCollectionViewにもそんな感じの背景色変える方法があるのでは？そもそもdecorationItemsは動的に変えられるものなのか？と疑問に思い調べたけど、動的に変える方法はなさそうだった。ChatGPTにも聞いてみたが、怒られた。
![スクリーンショット 2023-12-15 10.09.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/e2429541-070e-c6d1-0466-679a3f4bf2dd.png)
### UICollectionElementCategoryについて
UICollectionViewLayoutには、以下の要素が重要。
- `UICollectionElementCategory.cell`: セル
- `UICollectionElementCategory.supplementaryView`: ヘッダやフッタなどの補助的なView
- `UICollectionElementCategory.decorationView`: 区切り線や背景などの装飾View

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

https://qiita.com/imk2o/items/fa1f4cc0b4bbe4244226#uicollectionviewlayout-に登場する要素の種類

HeaderやFooterのようにsupplementaryViewを登録しておいて、動的に内容を変えられれば良いのかなー
### ちなみにDecorationViewを使用する方法については？
1. UICollectionReusableViewを継承したDecorationViewを作成

```swift
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を継承してカスタムクラス作成
以下は例なのでこんな感じというだけ

```swift
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を変更できるようにするらしい。

https://qiita.com/imk2o/items/fa1f4cc0b4bbe4244226#要素のレイアウト情報に区切り線のレイアウト情報を追加するには
## supplementaryViewを使用して背景色を動的に変えるやり方
1\. UICollectionReusableViewでカスタムクラス作成

```swift
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を継承してカスタムクラスを作成

```swift
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同様)

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

4\. Delegateメソッドの実装

```swift
// 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として使えるようにた例

https://github.com/devxoul/UICollectionViewFlexLayout/blob/master/Sources/UICollectionViewFlexLayout/UICollectionViewFlexLayout.swift

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

https://techlife.cookpad.com/entry/2017/06/29/190000

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

https://github.com/c0ming/SBCollectionViewFlowLayout/blob/master/SBCollectionViewFlowLayout/SBCollectionViewFlowLayout.swift
    

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

https://github.com/imk2o/UICatalog/blob/master/UICatalog/CollectionView/CollectionViewFlowLayoutWithDecorations.swift