안녕하세요!
오늘은 Modern Collection View 라는 것을 사용해보겠습니다.
Modern Collection View란?
Collection View는 아마 다들 접해보셨을텐데 Modern Collection View는 무엇이 다를까요?
공식문서를 통해 봤을 때 특징은 다음과 같습니다.
- iOS 14.0+
- Compositional layouts 사용
- Diffable data sources 사용
- 심플하게 UI 업데이트가 가능합니다.
그렇다면 Compositional layouts과 Diffable data sources은 무엇일까요?
Compositional layouts
Compositional layouts은 UICollectionViewLayout 타입으로
아래 그림처럼 Section, Group, Item으로 구성된 레이아웃입니다.
각 영역을 Section, Group, Item으로 나눈 덕분에
복잡하고 다양한 레이아웃에 대응할 수 있게 됩니다.
Diffable data sources
Diffable data sources는 CollectionView의 데이터를
관리하고 셀을 제공하는 클래스입니다.
Diffable data sources를 통해 데이터 변화를 추적해서
UI를 업데이트 해줍니다. 덕분에 애니메이션 처리가 자동으로 됩니다.
대신 데이터가 추적 가능하게 Hashable 해야합니다.
(더 자세한 설명들은 진행하면서 하겠습니다.)
장점
공식문서에서 예시 프로젝트를 다운받을 수도 있는데,
예시 프로젝트를 둘러고보 제가 느낀 Modern Collection View의 장점은 다음과 같습니다.
하나의 CollectionView로 복잡한 레이아웃 구성 가능
(여러 레이아웃이 섞인 CollectionView를 만들 수 있습니다.)
데이터 변화에 따른 동적인 뷰를 구성 가능
(변화에 따라 자동으로 애니메이션 처리가 됩니다.)
Modern Collection View 구성하기(순서 X)
0. 데이터 타입 선언, 인스턴스 생성
collectionView에 들어갈 데이터 타입을 선언해줍니다.
아까 설명했듯이 데이터 변화를 추적하기 위해 Hashable이 필수입니다!
struct Section: Hashable {
let index: String
}
enum Item: Hashable {
case firstLayout(ItemContents)
case secondLayout(ItemContents)
}
struct ItemContents: Hashable {
let index: Int
init(index: Int) {
self.index = index
}
}
1. CollectionView 선언
먼저 CollectionView를 클로져로 선언해줍니다.
기존의 CollectionView 선언과 동일합니다!
레이아웃은 화면 전체로 잡겠습니다.
(layout 구성을 위한 createLayout() 함수, FirstLayoutCell 등은 아래에서 진행됩니다.)
lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: self.createLayout())
collectionView.register(FirstLayoutCell.self, forCellWithReuseIdentifier: FirstLayoutCell.id)
collectionView.register(SecondLayoutCell.self, forCellWithReuseIdentifier: FirstLayoutCell.id)
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}
func setUI() {
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0),
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0),
])
}
2. CollectionViewCell 구성
cell 구성도 기존과 똑같습니다!
예시를 위해 cell 2개를 선언하겠습니다
import UIKit
class FirsetLayoutCell: UICollectionViewCell {
// cell 재활용 id
static let id = "FirsetLayoutCell"
private let indexTextLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setUI()
}
func setUI() {
addSubview(indexTextLabel)
indexTextLabel.translatesAutoresizingMaskIntoConstraints = false
indexTextLabel.backgroundColor = .black
indexTextLabel.textColor = .white
NSLayoutConstraint.activate([
indexTextLabel.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 10),
indexTextLabel.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 10),
indexTextLabel.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: -10),
indexTextLabel.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: -10),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
import UIKit
class SecondLayoutCell: UICollectionViewCell {
// cell 재활용 id
static let id = "SecondLayoutCell"
private let indexTextLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setUI()
}
func setUI() {
addSubview(indexTextLabel)
indexTextLabel.translatesAutoresizingMaskIntoConstraints = false
indexTextLabel.backgroundColor = .blue
indexTextLabel.textColor = .white
NSLayoutConstraint.activate([
indexTextLabel.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0),
indexTextLabel.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0),
indexTextLabel.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0),
indexTextLabel.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
3. UICollectionViewCompositionalLayout 구성
이제부터 조금씩 달라집니다.
UICollectionViewCompistionalLayout을 구성해줍니다.
switch문을 통해 Section에 따라 다른 레이아웃을 리턴해줬습니다.
아래 함수처럼 Section, Group, Item을 구성할 때 다양한 설정이 가능합니다.
func createLayout() -> UICollectionViewCompositionalLayout {
// 섹션 사이의 간격을 여기서 설정할 수 있습니다.
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 3
return UICollectionViewCompositionalLayout {[weak self] sectionIndex, _ in
switch sectionIndex {
case 0:
return self?.createFirstLayoutSection()
case 1:
return self?.createSecondSection()
default:
return self?.createFirstLayoutSection()
}
}
}
func createFirstLayoutSection() -> NSCollectionLayoutSection {
// fractionalWidth은 상대적인 크기(컬렉션 뷰 크기 기준)
// absolute는 고정값입니다.
// item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(200))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
// section
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
return section
}
func createSecondSection() -> NSCollectionLayoutSection {
// item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
// section
let section = NSCollectionLayoutSection(group: group)
return section
}
4. UICollectionViewDiffableDataSource 구성
크게 2가지 작업으로 나뉩니다.
- dataSource 설정
- NSDiffableDataSourceSnapShot 설정
dataSource를 설정하면서 cell 재활용 설정이 가능하고 데이터를 통해 셀 초기화를 진행합니다.
그리고 snapShot을 설정하면서 실질적으로 데이터를 넣습니다!
스냅샷은 뭘까요?
해석하면 스냅샷은 특정 시점의 뷰에서 데이터의 상태를 표현하는 것입니다.
콜렉션 뷰에 담길 형태로 스냅샷에 데이터를 넣고 적용하면 뷰에 적용됩니다.
그리고 데이터가 바뀔 때에도 변화를 뷰에 적용하려는 시점에 snapShot 설정이 필요합니다.
// 나중에 초기화 해주려고 옵셔널 선언
private var dataSource: UICollectionViewDiffableDataSource<Section,Item>?
func setDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .firstLayout(let itemContents):
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FirstLayoutCell.id, for: indexPath) as? FirstLayoutCell else {
return UICollectionViewCell()
}
// 여기서 개별 셀에 접근 가능
// ex) cell.textLabel.text = itemContents.index
return cell
case .secondLayout(let itemContents):
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SecondLayoutCell.id, for: indexPath) as? SecondLayoutCell else {
return UICollectionViewCell()
}
// 여기서 개별 셀에 접근 가능
// ex) cell.textLabel.text = itemContents.index
return cell
}
})
var snapShot = NSDiffableDataSourceSnapshot<Section,Item>()
snapShot.appendSections([Section(index: "0")])
let firstItems = [
Item.firstLayout(ItemContents(index: 0)),
Item.firstLayout(ItemContents(index: 1)),
Item.firstLayout(ItemContents(index: 2)),
Item.firstLayout(ItemContents(index: 3)),
Item.firstLayout(ItemContents(index: 4)),
Item.firstLayout(ItemContents(index: 5)),
Item.firstLayout(ItemContents(index: 6)),
Item.firstLayout(ItemContents(index: 7)),
Item.firstLayout(ItemContents(index: 8)),
Item.firstLayout(ItemContents(index: 9)),
]
snapShot.appendItems(firstItems, toSection: Section(index: "0"))
snapShot.appendSections([Section(index: "1")])
let secondItems = [
Item.secondLayout(ItemContents(index: 0)),
Item.secondLayout(ItemContents(index: 1)),
Item.secondLayout(ItemContents(index: 2)),
Item.secondLayout(ItemContents(index: 3)),
Item.secondLayout(ItemContents(index: 4)),
Item.secondLayout(ItemContents(index: 5)),
Item.secondLayout(ItemContents(index: 6)),
Item.secondLayout(ItemContents(index: 7)),
Item.secondLayout(ItemContents(index: 8)),
Item.secondLayout(ItemContents(index: 9)),
]
snapShot.appendItems(secondItems, toSection: Section(index: "1"))
dataSource?.apply(snapShot)
}
결과물
소감
개인적으로는 Modern Collection View를 구성하려면 선행 작업이 많아서 번거롭다고 생각합니다.
원래 공식 문서 목차를 따라가려고 했는데 길이 보이시나요?...
대신 틀만 잡아놓으면 섹션을 확장해서 구성하기 쉽고 무엇보다 데이터 변화를 자동을 추적해서 UI가 자연스럽게 업데이트 되는 것이 큰 장점이기 때문에 기존의 Collection View보다는 가급적 Modern Collection View 사용을 지향하는 것이 좋을 것 같습니다.
참고한 자료들
https://velog.io/@oasis444/Modern-Collection-Views