UICollectionViewDiffableDatasource 개발기
후기
- RxDataSource 나 기존 UICollectionViewLayout 으로 구현 불가능한 세로/가로 스크롤형 뷰를 생성 가능
- 이전 버전에 대한 대응 노하우가 쌓인다면 사용해볼만 함
- 지원하는 OS 버전이 낮을 수록 안정성이 떨어짐
- 최소버전을 15이상 지원한다면 사용해볼만 함
collectionView 생성
let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: createLayout()
)
Snapshot 생성
private typealias Snapshot = NSDiffableDataSourceSnapshot<SectionHeaderReactor, CellReactor>
var currentSnapshot = Snapshot()
- ReactorKit을 사용한다면 RxDataSource보다 호환성이 떨어지며 Snapshot의 역할이 모호해짐
- Reactor.State 는 Data, SnapShot 은 View 를 직접 관리 용도로 사용 함
ApplySnapshot
func applySnapshot(animatingDifferrences: Bool = true) {
guard let reactor else { return }
self.currentSnapshot = Snapshot()
reactor.currentState.sectionReactors.forEach { collection in
self.currentSnapshot.appendSections([collection])
}
self.dataSource?.apply(self.currentSnapshot,
animatingDifferences: animatingDifferrences)
}
- 버전에 따른 snapshot 적용 동작
- iOS 14 이하
- animatingDifferences : true : Diff
- animatingDifferences : false : ReloadData
- iOS 14 이하
ReloadCompletionHandler
func applySnapshot(animatingDifferrences: Bool = true, completion: (() -> Void)? = nil) {
//...
self.dataSource?.apply(self.currentSnapshot,
animatingDifferences: animatingDifferrences,
completion: completion)
}
Reactor Bind
public func bind(reactor: Reactor) {
reactor.state.map { $0.sectionReactors }
.distinctUntilChanged()
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.applySnapshot(animatingDifferrences: false)
})
.disposed(by: disposeBag)
}
- Bind 로 collectionViewDataSource를 연결하는 것이 아닌 방식
DataSource 생성
typealias DataSource = UICollectionViewDiffableDataSource<SectionHeaderReactor, CellReactor>
func configureDataSource() {
configureSections()
configureCells()
applySnapshot()
}
configureCell
func configureCells() {
typealias CellRegistration = UICollectionView.CellRegistration<Cell, CellReactor>
let cellRegistration = CellRegistration { cell, _, cellReactor in
cell.bind(reactor: cellReactor)
}
dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, cellReactor -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
for: indexPath,
item: cellReactor)
}
}
func configureSections() {
typealias SectionHeaderViewRegistration = UICollectionView.SupplementaryRegistration<SectionHeaderView>
let headerRegistration = SectionHeaderViewRegistration(
elementKind: SectionHeaderView.elementKind) { [weak self] supplementaryView, _, indexPath in
guard let self else { return }
let sectionReactor = self.currentSnapshot.sectionIdentifiers[indexPath.section]
supplementaryView.bind(reactor: sectionReactor)
supplementaryView.plusButtonTap
.subscribe(onNext: { [weak self] _ in
//
})
.disposed(by: supplementaryView.disposeBag)
dataSource?.supplementaryViewProvider = { collectionView, elementKind, indexPath in
return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}
}
reconfigureItems
-
iOS 15+ reconfigureItems(_:)
-
새로운 cell을 dequeuing and configuring 하지 않고 exisiting cell 을 업데이트
- 성능이 좋아짐,
UICollectionViewLayout 생성
func createLayout() -> UICollectionViewLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
let layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider, configuration: config)
return layout
}
createSection
func createSection(at sectionIndex: Int,
with group: NSCollectionLayoutGroup) -> NSCollectionLayoutSection {
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 8
section.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 12,
bottom: 12,
trailing: 12)
let sectionHeader = createSectionHeader()
section.boundarySupplementaryItems = [sectionHeader]
return section
}
interGroupSpacing
createGroup
func createGroup(at sectionIndex: Int,
with item: NSCollectionLayoutItem,
snapshot: Snapshot) -> NSCollectionLayoutGroup {
let groupSize: NSCollectionLayoutSize
if let sectionReactor = snapshot.sectionIdentifiers[safe: sectionIndex],
sectionReactor.currentState.cellReactors.isEmpty {
groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .absolute(0))
} else {
groupSize = NSCollectionLayoutSize(widthDimension: .estimated(1200),
heightDimension: .absolute(72))
}
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
return group
}
createItem
func createItem() -> NSCollectionLayoutItem {
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(100),
heightDimension: .absolute(72))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
return item
}
createSectionHeader
func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let titleSize = NSCollectionLayoutSize(widthDimension: .absolute(view.frame.width),
heightDimension: .absolute(48))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: titleSize,
elementKind: SectionHeaderView.elementKind,
alignment: .top)
if #available(iOS 15, *) {
sectionHeader.pinToVisibleBounds = true
sectionHeader.zIndex = 2
}
return sectionHeader
}
fractionalWidth(1)
를 사용하지 않은 이유section.contentInsets
상대로 사이즈가 잡혀서 원하는full size
가 잡히지 않음
pinToVisibleBounds
- iOS 14 이하에서 화면이 깨지는 버그가 있음
@available(iOS 13.0, *)
open var pinToVisibleBounds: Bool
open var zIndex: Int
sectionProvider
let sectionProvider = { [weak self] (sectionIndex: Int, _: NSCollectionLayoutEnvironment)
-> NSCollectionLayoutSection? in
guard let self else { return nil }
let item = createItem()
let group = createGroup(at: sectionIndex,
with: item,
snapshot: self.currentSnapshot)
let section = createSection(at: sectionIndex,
with: group,
snapshot: self.currentSnapshot)
return section
}
UISwipeActionsConfiguration
- 시뮬레이터에서 버그가 있음(Xcode 13 기준)
- 시뮬레이터에서 삭제 기능을 별도로 구현함
댓글남기기