Diffable에서 무한 스크롤을 구현해야하는데 구현하는 방법이 구글링해도 잘 안나옴;;
그래서 직접 구현하기로 했음.
보통 일반 CollectioView 에서 무한 스크롤을 구현하는 방법은
1. cell 개수를 Int.Max로 잡아서 스크롤 할 때마다 ReuseCell 로 구현하거나,
2. cell의 개수를 스크롤할 아이템에 배수로 정하고 cell을 스크롤할때마다 나머지 연산을 통해 보이는 cell을 강제로 이동시키는 방법으로 구현한다.
그래서 나도 2번 방법으로 구현하려하니 Compositinal Layout을 사용하면 Scroll Delegate를 사용하지 못하고, DataSource는 Diffable을 사용해서 구현하므로 많은 것이 제한 되었음;; 그래서 공식 문서를 보니 Compositional Layout을 정의하고, section 에서 Escaping Handler를 사용가능 했음(visibleItemInvalidationHandler). 이게 포커싱된 Cell이 바뀔때 마다 호출되는데, 잘 사용하면 무한 스크롤을 구현할 수 있겠다 싶었다.
private let list = [#colorLiteral(red: 0.9882352941, green: 0.8901960784, blue: 0.5411764706, alpha: 1), #colorLiteral(red: 0.5843137255, green: 0.8823529412, blue: 0.8274509804, alpha: 1), #colorLiteral(red: 0.9529411765, green: 0.5058823529, blue: 0.5058823529, alpha: 1)]
private var colorModels = [ColorModel]()
enum Section: Int, CaseIterable {
case Main
}
typealias DataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>
스크롤 될 때마다 색을 바꿀 수 있도록 UIColor List를 정의하고, SnapShot에 할당해줄 ColorModels를 정의했다. DataSource, SnapShot에 넣어줄 CollectionView Section을 정의했음. 왜 Model을 2개 정의했냐면, Diffable DataSource는 Hashable을 따르므로, 데이터를 넣을 때 각각 다른 identify를 가진 Model이 필요하므로 Custom Model을 만들어줌.
struct ColorModel: Hashable {
let identifier = UUID()
let color: UIColor
}
Color는 같지만 다른 identifier를 가지고 있는 Custom Model을 정의
private func bindSnapShotApply(section: Section, item: [AnyHashable]) {
DispatchQueue.global().sync {
guard var snapShot = self.dataSource?.snapshot() else { return }
item.forEach {
snapShot.appendItems([$0], toSection: section)
}
self.dataSource?.apply(snapShot, animatingDifferences: true) { [weak self] in
self?.collectionView.scrollToItem(at: [0, (self?.list.count ?? 1)],
at: .left,
animated: false)
}
}
}
private func configureSnapShot() {
var snapShot = Snapshot()
snapShot.appendSections([.Main])
self.dataSource?.apply(snapShot, animatingDifferences: true)
list.forEach { [weak self] in
self?.colorModels.append(ColorModel(color: $0))
}
var listItem = colorModels
for i in 0..<6 {
listItem.append(ColorModel(color: list[i % 3]))
}
self.bindSnapShotApply(section: .Main, item: listItem)
}
SnapShot을 정의할때 무한 스크롤을 만들기 위해 보여줄 아이템의 개수를 3배로 해서 무한스크롤이 가능하도록 일정 인덱스가 넘어가면 CollectionView의 보이는 아이템을 collectionView.scrollToItem를 사용해서 조정해줄거임.
private func configureCompositionalLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { (sectionNumber, env) -> NSCollectionLayoutSection? in
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.85)))
item.contentInsets = .init(top: 5, leading: 7, bottom: 5, trailing: 7)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.93),
heightDimension: .fractionalWidth(0.85)),
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.contentInsets = .init(top: 15, leading: 0, bottom: 15, trailing: 0)
section.visibleItemsInvalidationHandler = { [weak self] (visibleItems, offset, env) in
guard let currentIndex = visibleItems.last?.indexPath.row,
visibleItems.last?.indexPath.section == 0 else { return }
self?.willChangeMainSectionIndex(currentIndex: currentIndex)
}
return section
}
}
Compositional Layout을 정의할때 좌우 스크롤이 되는 것을 사용자 경험적으로 확인할 수 있도록 좌우 Cell이 조금 보이도록 Compositional Layout을 정의했다. 그 다음 중요한 부분이 section.visibleItemsInvalidationHandler를 정의하는 부분. visibleItems의 마지막 Model을 가져오면 그게 현재 포커싱 된 Cell이다. 따라서 해당 인덱스를 이용해 collectionView.scrollToItem를 사용하면 무한 스크롤이 가능하다.
private func willChangeMainSectionIndex(currentIndex: Int) {
switch currentIndex {
case self.colorModels.count - 1:
self.collectionView.scrollToItem(at: [0, self.colorModels.count * 2 - 1], at: .left, animated: false)
case self.colorModels.count * 2 + 1:
self.collectionView.scrollToItem(at: [0, self.colorModels.count], at: .left, animated: false)
default:
break
}
}
스크롤일 될때 일정 인덱스가 넘어가면 collectionView에 포커싱된 아이템을 강제로 바꾸는 부분. Model에 개수로 현재 인덱스가 아래로 내려가면 마지막으로 보내고, 위로 넘어가면 처음으로 보낸다. animated는 false로 정의해서 보이지 cell이 바뀌는게 보이지 않게 함.
private func configureDataSource() {
let datasource = DataSource(collectionView: self.collectionView, cellProvider: {(collectionView, indexPath, item) -> UICollectionViewCell in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier, for: indexPath) as? CollectionViewCell,
let item = item as? ColorModel else {
return UICollectionViewCell()
}
cell.updateView(item: item)
return cell
})
self.dataSource = datasource
self.collectionView.dataSource = datasource
}
DataSource에서 cell에 들어오는 item에 맞춰서 색깔을 업데이트 해주면 끝!
git
GitHub - MINRYUL/DiffableInfiniteScroll
Contribute to MINRYUL/DiffableInfiniteScroll development by creating an account on GitHub.
github.com
참고
Apple Developer Documentation
developer.apple.com
'iOS > 학습' 카테고리의 다른 글
iOS View 에서의 init 함수들 (0) | 2022.05.08 |
---|---|
iOS RxDataSource로 Custom Xib CalendarView 만들기 (0) | 2022.05.07 |
iOS에서의 Frame vs Bounds (0) | 2022.04.24 |
Storyboard 에서 Generic Type 의존성 주입하기 (0) | 2022.03.20 |
translatesAutoresizingMaskIntoConstraints (0) | 2022.02.13 |