Bring compositional layouts to your app and simplify updating your user interface with diffable data sources.
Apple 공식 문서의 설명을 보면
CompositionalLayout과 DiffableDataSource를 같이 사용하는 것을 ModernCollectionView라고 하는 것을 알 수 있습니다.
그렇다면 CompositionalLayout과 DiffableDataSource가 무엇인지 알아봐야겠죠?
CompositionalLayout을 사용하면 복잡한 레이아웃을 구현하고 유연하게 관리할 수 있습니다.
DiffableDataSource는 데이터를 관리하고 UI를 업데이트하는 역할을 합니다.
Data Source와 달리 데이터가 달라진 부분을 추적하여 자연스럽게 UI를 업데이트합니다.
DataSource | DiffableDataSource |
---|---|
출처: https://velog.io/@ellyheetov/UI-Diffable-Data-Source
DiffableDataSource는 다음 두 가지 generic type을 가집니다.
두 파라미터는 반드시 Hashable 해야 하므로 Hashable 프로토콜을 채택해야 합니다.
struct Section: Hashable {
let id: String
}
enum Item: Hashable {
case random(UserInfo)
case rank(UserInfo)
case near(UserInfo)
case new(UserInfo)
case pick(UserInfo)
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
아래와 같은 복잡한 뷰를 ModernCollectionView를 사용하여 구현해 보겠습니다.
쉽게 설명하기 위해 UI(CompositionalLayout)와 Data(DiffableDataSource)로 나눠서 설명을 하겠습니다.
final class MainPageView: UIView {
lazy var collectionView: UICollectionView = {
let view = UICollectionView(frame: .zero, collectionViewLayout: .init())
view.register(RankSectionCell.self,
forCellWithReuseIdentifier: RankSectionCell.identifier)
view.register(DefaultSectionCell.self,
forCellWithReuseIdentifier: DefaultSectionCell.identifier)
view.register(SectionHeaderView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: SectionHeaderView.identifier)
view.setCollectionViewLayout(collectionViewLayout(), animated: true)
view.refreshControl = refreshControl
view.showsVerticalScrollIndicator = false
return view
}()
init() {
super.init(frame: .zero)
backgroundColor = ThemeColor.backGroundColor
setLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
위의 코드에서 collectionView의 각 Cell과 HeaderView를 등록해 주고 setCollectionViewLayout
메서드로 Layout을 설정해 줍니다
private extension MainPageView {
func setLayout() {
self.addSubview(collectionView)
collectionView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
func collectionViewLayout() -> UICollectionViewCompositionalLayout {
UICollectionViewCompositionalLayout { sectionNum, env -> NSCollectionLayoutSection in
switch sectionNum {
case 0:
return self.randomSection()
case 1:
return self.rankSection()
case 2:
return self.newSection()
default:
return self.defaultSection()
}
}
}
}
위의 코드에서 collectionViewLayout
메서드는 sectionNum에 따라 각각 다른 Layout을 return 해줍니다.
private extension MainPageView {
func randomSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1), // group의 넓이와 동일
heightDimension: .fractionalHeight(1)) // group의 높이와 동일
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 15, leading: 10, bottom: 0, trailing: 10)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1), // section의 넓이와 동일
heightDimension: .estimated(400)) // 400이지만 변경 가능
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
section.contentInsets = .init(top: 10, leading: 0, bottom: 10, trailing: 0)
let sectionHeader = headerSection()
section.boundarySupplementaryItems = [sectionHeader]
return section
}
func rankSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1), // group의 넓이와 동일
heightDimension: .fractionalHeight(1)) // group의 높이와 동일
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 10, leading: 5, bottom: 0, trailing: 5)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.45), // section의 넓이에 45%
heightDimension: .estimated(250)) // 250이지만 변경 가능
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.contentInsets = .init(top: 10, leading: 5, bottom: 10, trailing: 5)
section.interGroupSpacing = 10 // group간 간격 10
let sectionHeader = headerSection()
section.boundarySupplementaryItems = [sectionHeader]
return section
}
func newSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1), // group의 넓이와 동일
heightDimension: .fractionalHeight(0.75)) // group의 높이에 75%
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 10, leading: 5, bottom: 0, trailing: 5)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5), // section의 넓이에 50%
heightDimension: .estimated(350)) // 350이지만 변경 가능
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.contentInsets = .init(top: 10, leading: 5, bottom: 10, trailing: 5)
section.interGroupSpacing = 10
let sectionHeader = headerSection()
section.boundarySupplementaryItems = [sectionHeader]
return section
}
func defaultSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1), // group의 넓이와 동일
heightDimension: .fractionalHeight(0.8)) // group의 넓이에 80%
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 10, leading: 5, bottom: 0, trailing: 5)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1/3), // section의 넓이에 1/3
heightDimension: .estimated(200)) // 200이지만 변경 가능
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.contentInsets = .init(top: 10, leading: 5, bottom: 10, trailing: 5)
section.interGroupSpacing = 10
let sectionHeader = headerSection()
section.boundarySupplementaryItems = [sectionHeader]
return section
}
func headerSection() -> NSCollectionLayoutBoundarySupplementaryItem {
let layoutSectionHeaderSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(30)) // 30만큼
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: layoutSectionHeaderSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top)
return sectionHeader
}
}
해당 코드는 각각의 Section의 Layout에 대해 정의한 코드입니다.
위에서 언급했듯이 CompositionalLayout은 Section, Group, Item으로 구성됩니다.
여기서 Size를 정의하는 방법이 3가지가 있는데
1. .absolute - 고정 크기
2. .estimated - 런타임에 변경
3. .fractional - 비율
각 Cell의 Size에 따라 적절한 Layout을 설정해 줍니다.
final class MainPageViewController: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
override func viewDidLoad() {
super.viewDidLoad()
setDataSource()
setHeader()
}
}
private extension MainPageViewController {
func setDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Item>(
collectionView: mainPageView.collectionView,
cellProvider: { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .random(let item):
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: DefaultSectionCell.identifier,
for: indexPath) as? DefaultSectionCell else { return UICollectionViewCell() }
cell.configure(data: item, nickNameOn: true)
return cell
case .new(let item), .near(let item), .pick(let item):
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: DefaultSectionCell.identifier,
for: indexPath) as? DefaultSectionCell else { return UICollectionViewCell() }
cell.configure(data: item, nickNameOn: false)
return cell
case .rank(let item):
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: RankSectionCell.identifier,
for: indexPath) as? RankSectionCell else { return UICollectionViewCell() }
cell.configure(data: item, index: indexPath.item)
return cell
}
})
}
func applyItems(data: MainItems) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
let randomSection = Section(id: SectionName.random.sectionID)
let rankSection = Section(id: SectionName.rank.sectionID)
let newSection = Section(id: SectionName.new.sectionID)
let nearSection = Section(id: SectionName.near.sectionID)
let pickSection = Section(id: SectionName.pick.sectionID)
[randomSection, rankSection, newSection, nearSection, pickSection].forEach {
snapshot.appendSections([$0])
}
snapshot.appendItems(data.random, toSection: randomSection)
snapshot.appendItems(data.rank, toSection: rankSection)
snapshot.appendItems(data.new, toSection: newSection)
snapshot.appendItems(data.near, toSection: nearSection)
snapshot.appendItems(data.pick, toSection: pickSection)
dataSource?.apply(snapshot)
}
}
데이터 적용을 위해 Snapshot
을 생성하고 적용해야 합니다.
appendSections
으로 Section을 등록하고
appendItems
으로 Item을 등록한 후 apply
메서드로 Snapshot을 적용시키면 됩니다!
전체 코드: https://github.com/z-wook/Catcher
참고 자료: https://github.com/z-wook/Shopping
https://gyuios.tistory.com/153
https://ios-development.tistory.com/945
https://velog.io/@ellyheetov/UI-Diffable-Data-Source