Modern Collection View [3] - Intro to Diffable Data Source | Search with Combine
Diffable DataSource
를 사용한 컬렉션 뷰 구현Diffable DataSource
구현Snapshot
함수 구현 private func setupCollectionView() {
collectionView = .init(frame: view.bounds, collectionViewLayout: listLayout)
collectionView.backgroundColor = .systemBackground
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
cellRegistration = UICollectionView.CellRegistration(
handler: { (cell: UICollectionViewListCell, _, character: Character) in
var content = cell.defaultContentConfiguration()
content.text = character.name
content.secondaryText = character.job
content.image = UIImage(named: character.imageName)
content.imageProperties.maximumSize = .init(width: 60, height: 60)
content.imageProperties.cornerRadius = 30
cell.contentConfiguration = content
})
headerRegistration = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader, handler: { [weak self] (header: UICollectionViewListCell, _, indexPath) in
guard let self = self else { return }
self.configureHeader(header, at: indexPath)
})
dataSource = .init(collectionView: collectionView, cellProvider: { [weak self] (collectionView, indexPath, item) -> UICollectionViewCell? in
guard let self = self else { return nil }
let cell = collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: item)
return cell
})
dataSource.supplementaryViewProvider = { [weak self] (collectionView, kind, indexPath) -> UICollectionReusableView in
guard let self = self else { return UICollectionReusableView() }
let header = collectionView.dequeueConfiguredReusableSupplementary(using: self.headerRegistration, for: indexPath)
return header
}
}
private func setupSnapshot(store: [SectionCharactersTuple]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Character>()
store.forEach { sectionCharacter in
let (section, characters) = sectionCharacter
snapshot.appendSections([section])
snapshot.appendItems(characters, toSection: section)
}
dataSource.apply(snapshot, animatingDifferences: true)
reloadHeaders()
}
Diffable
하게 처리하기 위한 스냅샷 적용 함수 private func reloadHeaders() {
collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader).forEach { [weak self] indexPath in
guard let header = collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: indexPath) as? UICollectionViewListCell else { return }
self?.configureHeader(header, at: indexPath)
}
}
@objc private func shuffleTapped() {
backingStore = backingStore.shuffled().map({ ($0.section, $0.characters.shuffled()) })
setupSnapshot(store: backingStore)
}
@objc private func resetTapped() {
backingStore = segmentedControl.selectedUniverse.sectionedStubs
setupSnapshot(store: backingStore)
}
private func configureHeader(_ headerView: UICollectionViewListCell, at indexPath: IndexPath) {
guard
let model = dataSource.itemIdentifier(for: indexPath),
let section = dataSource.snapshot().sectionIdentifier(containingItem: model) else { return }
let count = dataSource.snapshot().itemIdentifiers(inSection: section).count
var content = headerView.defaultContentConfiguration()
content.text = section.headerTitleText(count: count)
headerView.contentConfiguration = content
}
func updateSearchResults(for searchController: UISearchController) {
searchText.send(searchController.searchBar.text ?? "")
}
private func setupSearchTextObserver() {
searchText
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.map({ $0.lowercased() })
.map(filterAndSortData)
.sink(receiveValue: { [weak self] store in
self?.setupSnapshot(store: store)
})
.store(in: &cancellables)
}
private func filterAndSortData(text: String) -> [SectionCharactersTuple] {
guard !text.isEmpty else { return segmentedControl.selectedUniverse.sectionedStubs }
let filteredAndSortedData = backingStore.map { section, characters -> SectionCharactersTuple in
let characters = characters
.sorted(by: {$0.name < $1.name })
.filter({($0.name.lowercased().contains(text))})
return (section, characters)
}
.filter({!$0.characters.isEmpty})
return filteredAndSortedData
}
CellRegisttion
등과 함께 컬렉션 뷰를 쓰는 게 매우 깔끔하다! 스냅샷을 적용할 때 또한 별도의 데이터 소스에 직접 접근하기보다도, 주어진 컬렉션 뷰에 데이터를 주는 데이터 소스의 스냅샷으로부터 정보를 얻어오는 방식을 다시 한 번 체크!