はじめに
最近、AppStoreに公開されているアプリで「iOS13〜のサポート」というアプリが増えてきた印象です。
ってわけで、そろそろこの辺を今更ながらキャッチアップしていこうということで勉強したので、CollectionViewにバッジを表示する方法をまとめてみました。
WWDC2019の動画で紹介されている
こちらのセッションの30分くらいからバッジに関する説明がされています。
この説明は、iOS13〜のCompositionalLayouts
を使ってバッジを表示するという説明であり、実際にはこちらのコードだけでは動かないため、今回は、バッジを表示するために必要なすべてのコードをこちらの記事にまとめさせていただきました。
今回作る画面
実装
iOS13〜のCompositionalLayoutsの部分(動画で紹介されているところ)
CompositionalLayouts
に関しては、WWDC2019の動画、あるいはこちらの記事が大変わかりやすいです。
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の登録をしてあげます。
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
に関してはこんな感じで用意します。
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
}
}
全体のコード
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
もこれから勉強するので、キャッチアップできたら、記事を更新するなり、まとめ直すなりしていきたいと思います。