Search

DiffableDataSource, Snapshot를 사용해보자

 들어가며

프로젝트에서 Combine를 사용하게 되었습니다. Combine은 RxSwift처럼 Cocoa Framework를 위한 확장 라이브러리를 제공해주지 않습니다.
물론, CombineCocoa가 있지만 Combine를 선택한 이유도 “서드 파티를 최대한 쓰지 말자”였기에 CombineCocoa를 임포트하는 것은 아니라는 생각이 들어서 사용하지 않았습니다.
RxCocoa같은 라이브러리가 없으니 CollectionView 사용하기가 제일 불편했습니다. DataSource를 설정할 때, 외부에 배열 변수를 두고서 해당 변수가 바뀌면 CollectionView를 리로드하는 식으로 구현했어야 했습니다. RxCocoa에서 제공해주는 rx.items같은 게 없었기 때문에 Stream를 타고 들어온 item를 바로 DataSource에 넘겨줄 수가 없었죠.
여러 방법을 찾다가 UICollectionViewDiffableDataSource를 만나게 되었습니다.

 UICollectionViewDiffableDataSource

The object you use to manage data and provide cells for a collection view. 데이터를 관리하고 CollectionView의 Cell를 제공하는데 사용하는 개체
DiffableDataSource는 Collection View의 데이터 및 UI에 대한 업데이트를 간단하고 효율적인 방법으로 관리하는데 필요한 동작을 제공합니다.
그러면, DiffableDataSource는 어떻게 사용하는걸까요?
이전에 DataSource를 사용하는 것처럼 프로토콜을 준수해서 사용하지 않고 다른 방식으로 필요한 동작을 제공해줍니다. Document에서 제공하는 방식을 따라서 DiffableDataSource를 구현해보겠습니다.
1.
DiffableDataSource를 CollectionView에 연결해줍니다.
먼저, DiffableDataSource를 만들어 줍니다. DiffableDataSource에는 SectionIdentifierTypeItemIdentifierType를 넣어주어야 합니다. 각 타입은 Hashable프로토콜을 준수해야 합니다.
저는 Section이라는 열거형 타입을 하나 만들고 이를 SectionIdentifierType에 넣었습니다.
enum Section { case main }
Swift
복사
그리고 ItemIdentifierType에는 String를 넣어 줬어요. 나중에 String 값을 셀에 넣어주려고 합니다.
private var dataSource: UICollectionViewDiffableDataSource<Section, String>!
Swift
복사
그리고 UICollectionViewDiffableDataSource를 반환하는 메서드를 하나 만들고 그 안에다가 UICollectionViewDiffableDataSource 이니셜라이저 메서드를 넣어줬어요. 해당 메서드의 인수로는 DataSource를 사용할 UICollectionView를 넣을 수 있습니다. 저는 photoCollectionView를 사용할 거라서 해당 Collection View를 인수로 넣었습니다.
private func photoCollectionViewDataSource() -> UICollectionViewDiffableDataSource<Section, String> { return UICollectionViewDiffableDataSource( collectionView: self.photoCollectionView, cellProvider: // TODO: - Create Provider ) }
Swift
복사
2.
Cell Provider를 구현하여 CollectionView의 Cell를 구성합니다.
Cell Provider를 전달하면 각 셀을 구성하여 UI에 데이터를 표시하는 방법을 결정하게 됩니다.
UICollectionViewDiffableDataSource는 인수로 CollectionView과 함께 CellProvider를 받습니다. CellProvider는 collectionView, indexPath, item를 반환합니다. 해당 값을 가지고 Cell를 설정해주면 됩니다.
private func photoCollectionViewDataSource() -> UICollectionViewDiffableDataSource<Section, String> { return UICollectionViewDiffableDataSource( collectionView: self.photoCollectionView, cellProvider: { collectionView, indexPath, item -> UICollectionViewCell? in guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCollectionViewCell.identifier, for: indexPath) as? PhotoCollectionViewCell else { return UICollectionViewCell() } cell.configureCell(imageURL: item) return cell }) }
Swift
복사
우리가 원하는 Cell를 제공하는 DataSource가 만들어졌습니다. 이제 만들어둔 dataSource에 반환 값을 넣어줍시다.
private func configureDataSource() { self.dataSource = self.photoCollectionViewDataSource() }
Swift
복사
이제 configureDataSource 메서드를 ViewDidLoad에서 불러주면 됩니다.
하지만, 이건 DataSource를 만들어 주는 과정일 뿐입니다. 데이터의 현재 상태를 생성하고 이를 UI에 표시하기 위해서는 Snapshot이 필요합니다.

 NSDiffableDataSourceSnapshot

A representation of the state of the data in a view at a specific point in time. 특정 시점의 뷰에서 데이터의 상태를 나타내는 표현
이름 그대로 스냅샷입니다. 특정 시점에 뷰가 가지고 있는 데이터 상태를 나타내게 됩니다.
DataSource는 Snapshot를 사용해서 CollectionView, TableView에 데이터를 제공합니다.
Snapshot으로 View에 표시되는 데이터의 초기 상태를 설정하고, View에 표시되는 데이터의 변경 사항을 반영합니다. 즉, reload를 하지 않는 대신 스냅샷을 갈아 끼워줘야 하는 겁니다.
그러면, DataSource에서 갈아 끼우면서 사용할 Snapshot를 만들어 봅시다.
1.
Snapshot를 생성하고 표시할 데이터의 상태로 채워 줍니다.
먼저, Snapshot를 DataSource처럼 만들어 줍니다.
private var snapshot: NSDiffableDataSourceSnapshot<Section, String>!
Swift
복사
만든 Snapshot 안을 채워줘야 겠네요. Section은 이전에 만들었던 .main으로 설정해주고 dataSource에 Snapshot를 적용해줍니다(apply).
private func configureSnapshot() { self.snapshot = NSDiffableDataSourceSnapshot<Section, String>() self.snapshot.appendSections([.main]) self.dataSource.apply(self.snapshot, animatingDifferences: true) }
Swift
복사
해당 메서드는 DiffableDataSource를 설정하고 나서 생성될 수 있도록 아까 만들었던 configureDataSource 메서드에 넣어 줄게요.
private func configureDataSource() { self.dataSource = self.photoCollectionViewDataSource() self.configureSnapshot() }
Swift
복사
2.
UI 변경 사항을 반영하도록 Snapshot을 적용합니다.
위에 만든 Snapshot은 데이터의 초기 상태를 설정해준겁니다. 따라서, 새로운 데이터가 들어왔을 때의 상태를 만들어 주어야 합니다.
데이터가 리로드되면 우리는 이전에 있던 데이터를 지우고 새로운 데이터를 넣어줘야 합니다. 그리고 새로운 스냅샷을 DataSource에 넣어주면 됩니다. 이를 코드로 작성하면 이렇게 되겠죠?
private func reloadUrlList(_ items: [String]) { let previousImageUrls = self.snapshot.itemIdentifiers(inSection: .main) self.snapshot.deleteItems(previousImageUrls) self.snapshot.appendItems(items, toSection: .main) self.dataSource.apply(self.snapshot, animatingDifferences: true) }
Swift
복사

 마무리

이제 해당 코드를 CollectionView의 데이터가 들어왔을 때, 적용해주면 됩니다.
imageUrlPublisher .receive(on: RunLoop.main) .sink(receiveValue: { [weak self] items in guard let self = self else { return } self.reloadUrlList(item) }) .store(in: &cancelBag)
Swift
복사
그러면, 어떻게 되는지 볼까요?
가져온 이미지가 CollectionView에 잘 올라가는 걸 볼 수 있습니다.
다음에는 CollectionViewLayout를 사용해서 CollectionView의 Layout를 설정해보겠습니다.
위에 있는 DiffableDataSource 예시는 해당 레포에서 보실 수 있습니다.

 참고 자료