完成品
CompositionalLayout組む際に毎回どう実装するんだっけ?となるケースが多く、Appleのサンプルコードも参考になるんですが、ちょっと足りない要素があるので自分のリファレンス用にMusicアプリのUIを再現してメモ
※UIのレイアウトの再現なのでアーキテクチャなどは適用せずにViewにベタ書きしています
UICollectionViewDiffableDataSource
Assciated valuesなenumでセクションを管理しています
private enum Section {
case navigation(items: [Navigation])
case recentlyAddAlbums(items: [Album])
var ids: [AnyHashable] {
switch self {
case .navigation(let items): return items
case .recentlyAddAlbums(let albums): return albums.map { $0.id }
}
}
}
private enum Navigation: Hashable, CaseIterable {
case playlist
case artist
case album
case song
case recommend
case downloaded
}
private var dataSource: UICollectionViewDiffableDataSource<Int, AnyHashable>?
全体コード
import UIKit
class LibraryViewController: UIViewController {
private enum Section {
case navigation(items: [Navigation])
case recentlyAddAlbums(items: [Album])
var ids: [AnyHashable] {
switch self {
case .navigation(let items): return items
case .recentlyAddAlbums(let albums): return albums.map { $0.id }
}
}
}
private enum Navigation: Hashable, CaseIterable {
case playlist
case artist
case album
case song
case recommend
case downloaded
var cellContent: (UIImage?, String) {
switch self {
case .playlist: return (UIImage(systemName: "music.note.list"), "プレイリスト")
case .artist: return (UIImage(systemName: "music.mic"), "アーティスト")
case .album: return (UIImage(systemName: "square.stack"), "アルバム")
case .song: return (UIImage(systemName: "music.note"), "曲")
case .recommend: return (UIImage(systemName: "person.crop.square"), "あなたにおすすめ")
case .downloaded: return (UIImage(systemName: "arrow.down.circle"), "ダウンロード済み")
}
}
}
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Int, AnyHashable>?
private let recentlyAddAlbums: [Album] = (0..<40).map {
.init(name: "album \($0)", artist: .init(name: "artist name"), songs: [])
}
private var sections: [Section] {
[.navigation(items: Navigation.allCases),
.recentlyAddAlbums(items: recentlyAddAlbums)]
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "ライブラリ"
configureCollectionView()
// mock data append
var snapshot = NSDiffableDataSourceSnapshot<Int, AnyHashable>()
sections.enumerated().forEach {
snapshot.appendSections([$0.offset])
snapshot.appendItems($0.element.ids, toSection: $0.offset)
}
self.dataSource?.apply(snapshot)
}
private func configureCollectionView() {
// layout
self.collectionView = UICollectionView(frame: view.frame, collectionViewLayout: compositionalLayout())
self.collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view.addSubview(collectionView)
// cell register
let recentlyAddAlbumCellRegistration = UICollectionView.CellRegistration<MusicItemCell, Album> { cell, indexPath, item in
cell.configure(content: item)
}
let navigationCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Navigation> { cell, indexPath, item in
var content = cell.defaultContentConfiguration()
content.image = item.cellContent.0
content.imageProperties.tintColor = .systemPink
content.text = item.cellContent.1
content.textProperties.font = UIFont.preferredFont(forTextStyle: .title3)
cell.contentConfiguration = content
cell.accessories = [.disclosureIndicator()]
}
let headerRegistration = UICollectionView
.SupplementaryRegistration<HeaderSupplementaryView>(elementKind: UICollectionView.elementKindSectionHeader) { (supplementaryView, string, indexPath) in
supplementaryView.label.text = "最近追加した項目"
}
// datasource
dataSource = .init(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
switch self.sections[indexPath.section] {
case .recentlyAddAlbums(let items):
return collectionView.dequeueConfiguredReusableCell(using: recentlyAddAlbumCellRegistration,
for: indexPath,
item: items[indexPath.item])
case .navigation(let items):
return collectionView.dequeueConfiguredReusableCell(using: navigationCellRegistration,
for: indexPath,
item: items[indexPath.item])
}
})
dataSource?.supplementaryViewProvider = {
$0.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: $2)
}
collectionView.dataSource = dataSource
}
private func compositionalLayout() -> UICollectionViewCompositionalLayout {
UICollectionViewCompositionalLayout { (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
switch self.sections[sectionIndex] {
case .recentlyAddAlbums:
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(72.0),
heightDimension: .estimated(72.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(72.0))
let itemCount: Int
if layoutEnvironment.container.contentSize.width > 1000 {
itemCount = 4
} else if layoutEnvironment.container.contentSize.width > 600 {
itemCount = 3
} else {
itemCount = 2
}
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item,
count: itemCount)
let spacing = 16.0
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: spacing, bottom: 0, trailing: spacing)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(32))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top,
absoluteOffset: .init(x: 0, y: -spacing))
section.boundarySupplementaryItems = [sectionHeader]
return section
case .navigation:
let listSection: NSCollectionLayoutSection = .list(using: .init(appearance: .plain),
layoutEnvironment: layoutEnvironment)
listSection.contentInsets = .init(top: 0, leading: 0, bottom: 24, trailing: 0)
return listSection
}
}
}
}
// MARK: - Custom Cell
extension ViewController {
class AlbumCell: UICollectionViewCell {
let imageView = UIImageView()
let albumNameLabel = UILabel()
let artistNameLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
// layout
imageView.translatesAutoresizingMaskIntoConstraints = false
albumNameLabel.translatesAutoresizingMaskIntoConstraints = false
artistNameLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imageView)
contentView.addSubview(albumNameLabel)
contentView.addSubview(artistNameLabel)
NSLayoutConstraint.activate([
// imageView constraint
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
// albumNameLabel constraint
albumNameLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 5),
albumNameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
albumNameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
// artistNameLabel constraint
artistNameLabel.topAnchor.constraint(equalTo: albumNameLabel.bottomAnchor),
artistNameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
artistNameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
artistNameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
// Appearance
imageView.backgroundColor = .systemPink
imageView.layer.cornerRadius = 8
artistNameLabel.textColor = .secondaryLabel
}
func configure(_ album: Album) {
self.albumNameLabel.text = album.name
self.artistNameLabel.text = album.artist.name
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
class HeaderSupplementaryView: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor, constant: 24),
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor),
])
if let descriptor = UIFontDescriptor
.preferredFontDescriptor(withTextStyle: .title2)
.withSymbolicTraits(.traitBold) {
label.font = UIFont(descriptor: descriptor, size: 0)
}
}
required init?(coder: NSCoder) { fatalError() }
}
}
// MARK: - Data Model
struct Album: Identifiable {
let id: UUID = UUID()
let name: String
let artist: Artist
}
struct Artist {
let name: String
}