
✅ Diffable DataSource에 대해서 알아보기 전, 현재 CollectionView의 DataSource를 간단하게 살펴보고, 어떤 문제점이 있는지 간단하게 알아보자.
기본적으로 CollectionView를 사용하려면, 우리는 UICollectionViewDataSource 프로토콜을 채택해야된다.
weak var dataSource: UICollectionViewDataSource? { get set }
@MainActor
protocol UICollectionViewDataSource
dataSource는 collectionView에 데이터를 제공하는 역할을 한다. collectionView가 dataSource를 참조하며, 이를 통해 필요한 데이터를 요청하고 받는다. 참조 사이클을 방지하기 위해 dataSource는 약한 참조로 선언되어있다.// 특정 Section 안에 몇 개의 데이터가 존재하는가? (X번째 섹션에서 N개의 Cell을 만들 것이다)
func collectionView(UICollectionView, numberOfItemsInSection: Int) -> Int
// indexPath에 대한 Cell의 모양이 어떻게 생겼는가? (X번째 섹션에서, Y번째 셀이 어떤 모양인가?)
func collectionView(UICollectionView, cellForItemAt: IndexPath) -> UICollectionViewCell// Section이 몇 개로 구성되어 있는가?
func numberOfSections(in: UICollectionView) -> Intimport UIKit
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
private var data: [Int] = Array(1...10)
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController: UICollectionViewDataSource {
// [data]에 저장된 개수만큼 cell을 보여준다.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
data.count
}
// MyCollectionViewCell모양으로 Cell 모양을 구성한다.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.id, for: indexPath) as? MyCollectionViewCell else { fatalError("not found \(MyCollectionViewCell.id)") }
cell.configure(title: String("\(data[indexPath.row])번 째"))
return cell
}
}
✅ Data가 변경되었을 때, CollectionView UI를 업데이트 시키는 방법
numberOfItemsInSection, cellForItemAt이 다시 호출되어야한다. 이를 위해 우리는 reloadData()를 호출할 수 있다. Add Button: 제일 마지막에 새로운 데이터를 추가
Delete Button: 제일 마지막에 있는 데이터 삭제
@IBAction func didTappedAddButton(_ sender: UIButton) {
var value: Int
// count를 기반으로 한다면, 중복된 숫자가 나올 수 있기 때문에 마지막 아이템 숫자 + 1로 표현
if let item = data.last {
value = item + 1
} else {
value = 1
}
data.append(value)
// 변경된 데이터를 collectionView로 다시 로드한다
collectionView.reloadData()
}
@IBAction func didTappedDeleteButton(_ sender: UIButton) {
// 데이터가 없을 땐, 아무 동작도 하지 않도록 설정
guard !data.isEmpty else { return }
data.removeLast()
collectionView.reloadData()
}

✅ Delete Button의 Action을 수정해서, reloadData에 어떤 한계점이 있는지 확인해보자.
@IBAction func didTappedDeleteButton(_ sender: UIButton) {
guard !data.isEmpty else { return }
// 삭제 가능한 범위 중, 랜덤 아이템 삭제
let randomIndex = Int.random(in: 0 ..< data.count)
data.remove(at: randomIndex)
collectionView.reloadData()
}

reloadData() 메서드가 실행되면 전체 데이터를 다시 불러오지만, 특정 데이터가 어떻게 변화했는지에 대한 정보는 알 수 없다. 왜냐하면 numberOfItemsInSection과 cellForItemAt 메서드를 사용하여 전체 셀을 다시 그리기만 하기 때문이다.
즉, data 배열 안에 1이 삭제가 되든, 아무런 변화가 일어나지 않았든 reloadData를 호출하면 data의 전체 데이터를 기반으로 CollectionView의 UI를 업데이트 시키기 때문이다.
그렇다면 CollectionView에서 데이터의 변화를 애니메이션을 보여주고 싶다면 어떻게 해야할까?
insertItems(at:), deleteItems(at:), moveItem(at:to:) 메서드를 제공한다. 이 메서드들을 사용하면, 특정 인덱스에 항목이 추가, 삭제, 이동 되었음을 UICollectionView에게 명확하게 알려줄 수 있다. 이후 CollectionView에서 자체적으로 적절한 애니메이션을 제공하게 된다.@IBAction func didTappedDeleteButton(_ sender: UIButton) {
guard !data.isEmpty else { return }
let randomIndex = Int.random(in: 0 ..< data.count)
let indexPath = IndexPath(row: randomIndex, section: 0)
**data.remove(at: randomIndex)**
**collectionView.deleteItems(at: [indexPath])**
}

이 메서드는 전체 컬렉션 뷰를 업데이트 하는 것이 아니라, 매개변수로 전달받은 Index에 해당하는 셀만 업데이트 된다. 그렇기 때문에 잘못된 IndexPath를 전달하게 되면, 예상하지 않은 결과를 얻을 수도 있다. 그렇기 때문에 해당 메서드를 사용할 땐 IndexPath를 정의하는 순서가 매우 중요하다.
여러 개의 데이터가 한 번에 업데이트 되어야할 때, 설계와는 다른 애니메이션을 만나게 될 수도 있다. 코드를 통해 알아보자.
@IBAction func didTappedSwapButton(_ sender: UIButton) {
guard !data.isEmpty else { return }
data.swapAt(0, data.count - 1)
let startIndexPath = IndexPath(row: 0, section: 0)
let endIndexPath = IndexPath(row: data.count - 1, section: 0)
collectionView.moveItem(at: startIndexPath, to: endIndexPath)
collectionView.moveItem(at: endIndexPath, to: startIndexPath)
}

moveItem(at: startIndexPath, to: endIndexPath)을 실행시키면, 기존 1, 2, 3, 4, 5가 표현되어있던 CollectionView는 2, 3, 4, 5, 1을 표현하게 된다. 이후 moveItem(at: endIndexPath, to: startIndexPath)를 실행시키면 CollectionView는 Index를 기반으로 동작하기 때문에 다시 1, 2, 3, 4, 5의 순서로 CollectionView를 표현하게 되는 것이다.performBatchUpdates 를 통해서 진행하게 된다. 위 작업을 performBatchUpdates 클로저 내부로 이동시켜보자.@IBAction func didTappedSwapButton(_ sender: UIButton) {
guard !data.isEmpty else { return }
collectionView.performBatchUpdates {
let startIndexPath = IndexPath(row: 0, section: 0)
let endIndexPath = IndexPath(row: data.count - 1, section: 0)
collectionView.moveItem(at: startIndexPath, to: endIndexPath)
collectionView.moveItem(at: endIndexPath, to: startIndexPath)
data.swapAt(0, data.count - 1)
}
}

@IBAction func didTappedAddButton(_ sender: UIButton) {
DispatchQueue.global().async { [self] in
var value: Int
var indexPaths = [IndexPath]()
for _ in 0..<10 {
if let item = data.last {
value = item + 1
} else {
value = 1
}
let indexPath = IndexPath(row: data.count, section: 0)
data.append(value)
indexPaths.append(indexPath)
}
DispatchQueue.main.async { [self] in
collectionView.performBatchUpdates({
collectionView.insertItems(at: indexPaths)
}, completion: nil)
}
}
}
@IBAction func didTappedDeleteButton(_ sender: UIButton) {
DispatchQueue.global().async { [self] in
guard !data.isEmpty else { return }
var indexPaths = [IndexPath]()
for _ in 0 ..< min(data.count, 3) {
let randomIndex = Int.random(in: 0 ..< data.count)
let indexPath = IndexPath(row: randomIndex, section: 0)
data.remove(at: randomIndex)
indexPaths.append(indexPath)
}
DispatchQueue.main.async { [self] in
collectionView.performBatchUpdates({
collectionView.deleteItems(at: indexPaths)
}, completion: nil)
}
}

Invalid batch updates detected 에러가 발생하며 앱이 종료되었다.
Thread 1: "Invalid batch updates detected: the number of sections and/or items returned by the data source before and/or after performing the batch updates are inconsistent with the updates.\nData source before updates = { 1 section with item counts: [6] }\nData source after updates = { 1 section with item counts: [3] }\nUpdates = [\n\tDelete item (0 - 3),\n\tDelete item (0 - 1),\n\tDelete item (0 - 1)\n]"
diffable Datasource는 기존 dataSource의 문제점을 해결하는 새로운 dataSource이다.
즉 data의 관리와 UI 업데이트가 분리되어있지 않으며, data가 변경되면 관련 Animation이 자동으로 계산되어 View에 표현해준다.
diffableDataSource에 대한 개념과 사용 방법에 대해서 다음 포스팅을 통해 알아보고자 한다.