LoginSignup
1
1

More than 1 year has passed since last update.

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

Posted at

はじめに

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

WWDC2019の動画で紹介されている

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

今回作る画面

実装

iOS13〜のCompositionalLayoutsの部分(動画で紹介されているところ)

CompositionalLayoutsに関しては、WWDC2019の動画、あるいはこちらの記事が大変わかりやすいです。

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を使って、先ほど設定したレイアウトが表示されるようにしてあげる必要があります。

viewDidLoaddidSetなどどこでも良いので、バッジに使うViewの登録をしてあげます。

collectionView.register(BadgeView.self, forSupplementaryViewOfKind: ElementKind.badge, withReuseIdentifier: "BadgeView")

TableViewやCollectionViewのお決まりのパターンですが、こんな感じでバッジに表示する値などを設定します。

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もこれから勉強するので、キャッチアップできたら、記事を更新するなり、まとめ直すなりしていきたいと思います。

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