iOS13〜のCompositionalLayoutsでバッジを表示する

# はじめに
最近、AppStoreに公開されているアプリで「iOS13〜のサポート」というアプリが増えてきた印象です。
ってわけで、そろそろこの辺を今更ながらキャッチアップしていこうということで勉強したので、CollectionViewにバッジを表示する方法をまとめてみました。

# WWDC2019の動画で紹介されている
https://developer.apple.com/videos/play/wwdc2019/215

こちらのセッションの30分くらいからバッジに関する説明がされています。
この説明は、`iOS13〜のCompositionalLayouts`を使ってバッジを表示するという説明であり、実際にはこちらのコードだけでは動かないため、今回は、バッジを表示するために必要なすべてのコードをこちらの記事にまとめさせていただきました。

# 今回作る画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/64a04cef-137a-0032-27b0-1499e7e1a306.png" width=300>

# 実装
### iOS13〜のCompositionalLayoutsの部分（動画で紹介されているところ）
`CompositionalLayouts`に関しては、WWDC2019の動画、あるいはこちらの記事が大変わかりやすいです。

https://qiita.com/shiz/items/a6032543a237bf2e1d19#uicollectionviewflowlayout

```ViewController.swift
struct ElementKind {
    static let badge = "badge-element-kind"
}

private extension ViewController {
   func createLayout() -> UICollectionViewLayout {
        // バッジ表示位置の指定（今回の場合TopのTrailingに表示 = 右上）fractionalOffsetを指定することで、枠外へ飛び出させる。
        let badgeAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing], fractionalOffset: CGPoint(x: 0.3, y: -0.3))
        let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20), heightDimension: .absolute(20))
        let badge = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: badgeSize, elementKind: ElementKind.badge, containerAnchor: badgeAnchor)
        
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                              heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])
        
        item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                               heightDimension: .fractionalWidth(0.2))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
                                                       subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 8, bottom: 0, trailing: 8)
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }
}
```

`NSCollectionLayoutBoundarySupplementaryItem(layoutSize: badgeSize, elementKind: ElementKind.badge, containerAnchor: badgeAnchor)`
の`elementKind`には、任意の文字列を設定するのですが、後々出てくる他の項目と揃える必要があるので、`Struct`で定義しておきます。

これを`viewDidLoad`などでこんな感じで呼び出せばレイアウトの作成は完了です。
`collectionView.collectionViewLayout = createLayout()`

### バッジを表示する、バッジに表示する項目を設定する
上記のコードだけでは、バッジが表示されません。`CollectionViewDatasource`を使って、先ほど設定したレイアウトが表示されるようにしてあげる必要があります。

`viewDidLoad`や`didSet`などどこでも良いので、バッジに使うViewの登録をしてあげます。
```swift
collectionView.register(BadgeView.self, forSupplementaryViewOfKind: ElementKind.badge, withReuseIdentifier: "BadgeView")
```

TableViewやCollectionViewのお決まりのパターンですが、こんな感じでバッジに表示する値などを設定します。
```swift
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if let badge = collectionView.dequeueReusableSupplementaryView(ofKind: ElementKind.badge, withReuseIdentifier: "BadgeView", for: indexPath) as? BadgeView {
        badge.configuration(text: "\(indexPath.row)")
        return badge
    }
    return UICollectionReusableView()
}
```
`BadgeView`に関してはこんな感じで用意します。
```BadgeView.swift
import UIKit

class BadgeView: UICollectionReusableView {
    
    private let badgeLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
                
        badgeLabel.backgroundColor = .red
        badgeLabel.textAlignment = .center
        badgeLabel.font = .systemFont(ofSize: 12, weight: .bold)
        self.addSubview(badgeLabel)
        badgeLabel.translatesAutoresizingMaskIntoConstraints = false
        badgeLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
        badgeLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
        badgeLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
        badgeLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
    }
    // バッジを角丸にする
    override func draw(_ rect: CGRect) {
        badgeLabel.layer.cornerRadius = badgeLabel.frame.size.width / 2
        badgeLabel.clipsToBounds = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configuration(text: String) {
        badgeLabel.text = text
    }
}
```

## 全体のコード
```ViewContoller.swift
struct ElementKind {
    static let badge = "badge-element-kind"
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.collectionViewLayout = createLayout()
    }
    
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.register(UINib(nibName: "SimpleTextCell", bundle: nil), forCellWithReuseIdentifier: "SimpleTextCell")
            collectionView.register(BadgeView.self, forSupplementaryViewOfKind: ElementKind.badge, withReuseIdentifier: "BadgeView")
        }
    }
}

// MARK: - private method

private extension ViewController {
  
   func createLayout() -> UICollectionViewLayout { 
       // 上の方でコードを載せたのでここの中身は省略
   }
}

// MARK: - UICollectionViewDataSource

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        20
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        1
    }
    
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        if let badge = collectionView.dequeueReusableSupplementaryView(ofKind: ElementKind.badge, withReuseIdentifier: "BadgeView", for: indexPath) as? BadgeView {
            badge.configuration(text: "\(indexPath.row)")
            return badge
        }
        return UICollectionReusableView()
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SimpleTextCell", for: indexPath) as? SimpleTextCell else {
            return UICollectionViewCell()
        }
        cell.configuration(text: "\(indexPath.row)")
        cell.backgroundColor = indexPath.row % 2 == 0 ? .green : .gray
        return cell
    }
}
```

完成です。意外と少ないコードで実装できて、いい感じです🤩

# おわりに
`Diffable Data Source`もこれから勉強するので、キャッチアップできたら、記事を更新するなり、まとめ直すなりしていきたいと思います。