Modern Collection Views by example - Gaetano Matonti - Swift Heroes 2022
달라진
아이템만을 연산 후 조정(추가, 삭제, 변경 등)import Foundation
import UIKit
enum Section: Int, CaseIterable {
case locations
case favorites
}
enum Item: Hashable {
case header(String)
case item(String)
}
class ListDataSource: UICollectionViewDiffableDataSource<Section, Item> {
convenience init(collectionView: UICollectionView) {
let headerRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, _, item in
guard case let .header(title) = item else { return }
var contentConfiguration = cell.defaultContentConfiguration()
contentConfiguration.text = title
cell.contentConfiguration = contentConfiguration
cell.accessories = [.outlineDisclosure()]
}
let itemRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, _, item in
guard case let .item(title) = item else { return }
var contentConfiguration = cell.defaultContentConfiguration()
contentConfiguration.text = title
cell.contentConfiguration = contentConfiguration
cell.accessories = [.outlineDisclosure()]
}
self.init(collectionView: collectionView) { collectionView, indexPath, item in
switch item {
case .header:
return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: item)
case .item:
return collectionView.dequeueConfiguredReusableCell(using: itemRegistration, for: indexPath, item: item)
}
}
}
}
import UIKit
class ListViewController: UICollectionViewController {
private lazy var dataSource = ListDataSource(collectionView: collectionView)
convenience init() {
var listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar)
listConfiguration.headerMode = .firstItemInSection
let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
self.init(collectionViewLayout: layout)
applyInitialSnapshot()
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
title = "List"
}
private func applyInitialSnapshot() {
var locationSectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let locationHeaderItem: Item = .header("Locations")
locationSectionSnapshot.append([locationHeaderItem])
locationSectionSnapshot.append(
[
.item("Mac"),
.item("iPhone"),
.item("iPad")
],
to: locationHeaderItem
)
var favoriteSectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let favoriteHeaderItem: Item = .header("Favorites")
favoriteSectionSnapshot.append([favoriteHeaderItem])
favoriteSectionSnapshot.append(
[
.item("Docs"),
.item("iCloud")
],
to: favoriteHeaderItem
)
dataSource.apply(locationSectionSnapshot, to: .locations)
dataSource.apply(favoriteSectionSnapshot, to: .favorites)
}
}
UICollectionViewCompositionalLayout
이 제공하는 list
프로퍼티를 통해 레이아웃 구성Diffable DataSource
를 구현하기 위한 스냅샷 적용import UIKit
class AppStoreViewController: UICollectionViewController {
private var models: [String] = Array(repeating: "Lorem Ipsum", count: 100)
convenience init() {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(64))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: .fixed(8), trailing: nil, bottom: .fixed(8))
let groupItemCount = 3
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.92), heightDimension: .estimated(itemSize.heightDimension.dimension * CGFloat(groupItemCount)))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, repeatingSubitem: item, count: groupItemCount)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
let layout = UICollectionViewCompositionalLayout(section: section)
self.init(collectionViewLayout: layout)
title = "App"
collectionView.register(AppStoreCollectionViewCell.self, forCellWithReuseIdentifier: AppStoreCollectionViewCell.identifier)
}
}
extension AppStoreViewController {
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AppStoreCollectionViewCell.identifier, for: indexPath) as? AppStoreCollectionViewCell else { fatalError() }
let model = models[indexPath.row]
cell.configure(with: model)
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return models.count
}
}
CompositionalLayout
을 통해 아이템, 그룹, 섹션 간의 복잡한 레이아웃 가능
import UIKit
class AppStoreCollectionViewCell: UICollectionViewCell {
static let identifier = "AppStoreCollectionViewCell"
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 12
imageView.backgroundColor = .label
return imageView
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.font = .systemFont(ofSize: 18, weight: .bold)
label.numberOfLines = 1
return label
}()
private let subtitleLabel: UILabel = {
let label = UILabel()
label.textColor = .darkGray
label.font = .systemFont(ofSize: 15, weight: .semibold)
return label
}()
private let downloadButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(systemName: "icloud.and.arrow.down"), for: .normal)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setUI() {
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(downloadButton)
contentView.addSubview(subtitleLabel)
imageView.translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
downloadButton.translatesAutoresizingMaskIntoConstraints = false
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
downloadButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true
downloadButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
downloadButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
downloadButton.widthAnchor.constraint(equalTo: downloadButton.heightAnchor).isActive = true
titleLabel.topAnchor.constraint(equalTo: imageView.topAnchor).isActive = true
titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: downloadButton.leadingAnchor, constant: -16).isActive = true
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5).isActive = true
subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor).isActive = true
subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor).isActive = true
}
func configure(with model: String) {
titleLabel.text = model
subtitleLabel.text = model
}
}
Diffable DataSource
,CompositionalLayout
등은 iOS 13 이후부터 지원되는 다소 '핫'한 스택이지만, 그 이름처럼 기존 컬렉션 뷰에 비해 생각해야 할 부분이 많다. 어렵지만 그 이름처럼 '모던'한 형태를 구현하려면 필수다. 비록 컨퍼런스 영상을 이해하기에는 아직 벅찼지만 찬찬히 공부해보면서 다듬어보자.