일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- IOS
- human interface guidelines
- viewcontroller
- NotificationCenter
- View Life Cycle
- Codegen
- NumberFormatter
- Info.plist
- SWIFTUI
- delegation
- 독서후기
- SWIFT
- contentInset
- @available
- 야곰아카데미
- Structures and Classes
- UIResponder
- 독후감
- 책후기
- 스타트업주니어로살아남기
- 부트캠프
- 아이폰
- Mock
- roundingMode
- Failed to register bundle identifier
- 스위프트
- xcode
- Navigation
- mvvm
- Modality
- Today
- Total
호댕의 iOS 개발
[iOS] DiffableDataSource, CompositionalLayout를 통해 CollectionView 구현하기 본문
[iOS] DiffableDataSource, CompositionalLayout를 통해 CollectionView 구현하기
호르댕댕댕 2022. 6. 8. 00:22Implementing Modern Collection Views 문서를 보고 작성한 내용입니다.
기존 CollectionView를 구현하기 위해선 UICollectionViewDataSource, UICollectionViewDelegate를 주로 사용했다.
(TableView도 마찬가지이다)
iOS 13.0 이후에는 UICollectionViewDiffableDataSource, UICollectionViewCompositionalLayout을 사용해서 CollectionView를 구현할 수 있다.
(다만 UICollectionView.CellRegistration이 iOS 14.0부터 가능하다. 이게 뭔지는 밑에서 알아보자~)
- Compositional Layout: CollectionView의 레이아웃을 유연하고 빠르게 만들 수 있다. 콘텐츠에 대해 어떤 종류의 배치도 구현할 수 있다.
- Diffable DataSource: CollectionView의 데이터와 UI에 대한 업데이트를 간단하고 효율적으로 관리해주는 특별한 유형의 DataSource이다.
1️⃣ Compositional Layout 구현하기
UICollectionViewCompositionalLayout을 활용하여 Item, Group의 크기를 정해주고, 이를 통해 item, group, section을 정의해야 한다.
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = SectionKind(rawValue: sectionIndex) else { return nil }
let columns = sectionLayoutKind.columnCount
// The group auto-calculates the actual item width to make
// the requested number of columns fit, so this widthDimension is ignored.
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)
let groupHeight = columns == 1 ?
NSCollectionLayoutDimension.absolute(44) :
NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
return section
}
return layout
UICollectionViewCompositionalLayout의 클로저를 통해 UICollectionViewLayout을 생성할 수 있다.
생성한 Layout의 경우 CollectionView의 collectionViewLayout에 넣어주면 된다.
private func configureCollectionView() {
collectionView.translatesAutoresizingMaskIntoConstraints = false
let layout = createLayout()
collectionView.collectionViewLayout = layout
}
클로저의 파라미터는 각 섹션의 Index와, 현재 CollectionView의 크기 등을 알 수 있는 NSCollectionLayoutEnvironment를 받는다.
sectionIndex를 통해 특정 Section의 columnCount를설정할 수 있다. SectionKind는 현재 열거형으로 선언해놓은 Section의 종류를 의미한다.
해당 enum 내부에 연산 프로퍼티를 통해 columnCount를 지정해두면 Section에 따라 columnCount가 다르더라도 각각에 맞는 columnCount를 반환할 수 있다.
private enum SectionKind: Int {
case main
var columnCount: Int {
switch self {
case .main:
return 1
}
}
}
위 UICollectionViewCompositionalLayout의 클로저 부분을 보면 item, group에 대한 크기를 정할 때 사용할 수 있는 것이 3가지가 존재한다.
- .fractionalWidth / .fractionalHeight : 상위에 자신을 포함하고 있는 것에 대해 비율로 크기와 높이를 정한다. 즉, item의 경우 group에 포함되어 있기 때문에 group에 대한 비율로 크기를 정하는 것이다.
- .absolute : 절대적인 크기를 정해준다.
- .estimated : 일단 기본적인 크기로 지정한 크기가 되고 이후 셀의 크기를 계산하여 크기와 높이를 정한다.
지금까지 프로젝트를 하며 .absolute의 경우 다양한 기기에 대해 대응이 어려워 사용해보진 않았고, .estimated는 콘텐츠의 사이즈에 따라 Cell의 크기를 정해주고 싶을 때 사용했다.
.fractionalWidth / .fractionalHeight의 경우 화면 크기의 일정 비율대로 크기를 정해주고 싶은 경우 사용했다. 다만 이를 사용할 경우 내부 컨텐츠의 사이즈는 무시됐다.
상황에 따라 적절하게 사용해주면 될 것 같다.
만약 인셋을 주고 싶은 경우 .contentInset 프로퍼티를 통해 주면 된다. 이때 타입은 NSDirectionalEdgeInsets이다.
🚨 만약 column의 갯수가 정해져 있지 않고 Item의 수만큼 group을 생성하고 싶을 때는 어떻게 해야할까?
이때를 위해 NSCollectionLayoutgroup의 다른 이니셜라이저가 존재한다.
NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
subitems라는 파라미터에 배열 형태로 item을 넣어주면 알아서 item의 수만큼 group을 생성하게 된다.
🤯 Compositional Layout을 사용하며 만났던 문제 상황
이렇게 Compositional Layout을 사용해 UICollectionViewLayout을 생성하고 이를 collectionView의 collectionViewLayout에 할당해 주었다. 그리고 Rx의 bind 오퍼레이터를 활용해 collectionView의 아이템에 필요한 아이템도 전부 넣어주었다.
private func configureAppItems(with appItems: Observable<App>) {
appItems
.observe(on: MainScheduler.instance)
.flatMap { [weak self] app -> Observable<[String]> in
self?.titleStackView.apply(
thumnail: app.artworkUrl100,
name: app.trackName,
producer: app.artistName,
price: app.formattedPrice
)
self?.summaryScrollView.apply(with: app)
self?.descriptionTextView.text = app.description
return Observable.just(app.screenshotUrls)
}
.bind(to: screenshotCollectionView.rx.items(
cellIdentifier: String(describing: ScreenshotCell.self),
cellType: ScreenshotCell.self
)) { row, item, cell in
cell.apply(screenshotURL: item)
}
.disposed(by: disposeBag)
}
그런데 아무리 해도 Cell의 init이 호출되지 않는 문제가 있었다.
(참고로 해당 Cell의 경우 imageView 하나만을 표시하는 Cell이었다)
breakpoint를 찍어서 확인해보니 bind 오퍼레이터의 클로저 내부가 아예 타지 않았다. 호출 순서도 왜인지 bind 먼저 호출이 되고 이전 클로저에 있는 Observable.just를 return하는 코드가 호출이 됐다.
멘붕이었다... 분명 잘 한 것 같은데... 이렇게 했을 때 됐는데?
원인은 CollectionView의 높이가 안 잡혀 있어 아예 Cell을 초기화하지도 않는 것이었다.
왠지... CollectionView의 높이가 ViewDebugger로 봤을 때 계속 0이더라니... ㅎ...
그래서 CollectionView의 높이를 너비의 비율대로 줬더니 잘 떴다.
(혹시 이런 문제 겪으시면 CollectionView의 높이 Layout을 줘보세요!)
2️⃣ DiffableDataSource 설정하기
이렇게 CollectionView의 Layout을 잡아줬다면 이제는 여기에 들어올 데이터를 정해줘야 한다.
여기서 완전 처음에 말했던 CellRegistration을 해줘야 한다. 다만 이걸 사용하려면 iOS 14.0 이상이어야 한다.
private func configureCellRegistrationAndDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ListCell, HashableApp> { [weak self] cell, indexPath, hashable in
guard let weakSelf = self else { return }
cell.apply(
viewModel: weakSelf.cellViewModel,
app: hashable.app
)
}
diffableDataSource = UICollectionViewDiffableDataSource<SectionKind, HashableApp>(
collectionView: collectionView,
cellProvider: { collectionView, indexPath, app in
guard let sectionKind = SectionKind(rawValue: indexPath.section) else {
return UICollectionViewCell()
}
switch sectionKind {
case .main:
return collectionView.dequeueConfiguredReusableCell(
using: cellRegistration,
for: indexPath,
item: app
)
}
})
}
여기서 Cell을 등록해줘야 한다. 기존에는 UICollectionView의 register 메서드를 이용해 해줬던 것과 유사하다.
func register(
_ cellClass: AnyClass?,
forCellWithReuseIdentifier identifier: String
)
다만 CellRegistration을 해줄 때에는 등록해줄 Cell의 타입은 작성해주지만 identifier 대신 Cell에 들어가는 데이터의 타입을 적어준다.
이렇게 한 뒤 클로저를 열면 cell, indexPath, cell에 넣을 item을 인자로 받을 수 있다. 여기서 미리 cell에 어떤 item을 넣을지 정해주면 된다.
위 예시에선 viewModel과 app을 apply 메서드를 통해 주입해주었다.
그리고 UICollectionViewDiffableDataSource를 만들어주면 된다.
init(collectionView: UICollectionView, cellProvider: UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider)
생성자는 위와 같다.
그리고 DiffableDataSource를 초기화하여 설정해주는 것도 viewDidLoad에서 호출해 처리해주면 된다.
그럼 Modern CollectionView를 구현할 기본 준비는 끝났다.
3️⃣ Snapshot을 적용하자
private func configureSearchedApps(with searchedApps: Observable<[HashableApp]>) {
searchedApps
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] apps in
self?.loadingActivityIndicator.stopAnimating()
self?.configureSnapshot(with: apps)
self?.collectionView.setContentOffset(CGPoint.zero, animated: true)
})
.disposed(by: disposeBag)
}
private func configureSnapshot(with apps: [HashableApp]) {
snapshot = NSDiffableDataSourceSnapshot<SectionKind, HashableApp>()
snapshot.appendSections([.main])
snapshot.appendItems(apps)
diffableDataSource.apply(snapshot)
}
snapshot의 경우 NSDiffableDataSourceSnapshot을 생성하고 여기에 Section과 Item을 넣어주면 된다.
그리고 마지막으로 앞서 생성했던 diffableDataSource에 apply 메서드를 통해 snapshot을 적용해주면 끝이다.
여기서 한가지 주의해야 할 점은 만약 Section이 여러개라면 하나의 Section을 추가하고 items를 추가한 뒤 다시 Section을 추가하고 items를 추가해줘야 한다는 것이다.
private func configureInitialSnapshotWith(listProducts: [UniqueProduct], bannerProducts: [UniqueProduct]) {
snapshot = NSDiffableDataSourceSnapshot<SectionKind, UniqueProduct>()
snapshot.appendSections([.banner])
snapshot.appendItems(bannerProducts)
snapshot.appendSections([.list])
snapshot.appendItems(listProducts)
dataSource.apply(snapshot, animatingDifferences: true)
}
이렇게 하면 여러 Section의 CollectionView도 만들 수 있다.
🚨 만약 추가적으로 데이터를 더 불러와서 이를 추가하려면 어떻게 해야 할까?
그건 더욱 간단하다.
private func applySnapshot(with nextSearchedApps: [HashableApp]) {
snapshot.appendItems(nextSearchedApps, toSection: .main)
diffableDataSource.apply(snapshot, animatingDifferences: true)
}
appendItems 메서드를 사용해 추가를 해주고 diffableDataSource에 다시 snapshot을 apply해주면 된다.
'Software Engineering > iOS' 카테고리의 다른 글
[iOS] 커스텀 폰트를 적용해보자 (0) | 2022.08.10 |
---|---|
[iOS] 코드를 통해 그라데이션 효과 주기 (CAGradientLayer) (0) | 2022.07.16 |
[TableView] 코드로 TableView 구현하기, convenience init이 호출이 안된다?! (+ register와 dequeueReusableCell 쉽게 하기) (0) | 2022.05.31 |
[iOS] ActivityViewController를 커스텀하고, 이를 통해 공유하기 (0) | 2022.05.30 |
[iOS] 프레임워크(UIKit, Foundation 등)와 라이브러리, Static Framework와 Dynamic Framework (0) | 2022.05.05 |