Closed hcn1519 closed 2 years ago
Diffable DataSource
는 유용하게 사용할 수 있지만, 적용시 고려해야 할 것들이 있습니다. 여기에서는 Diffable DataSource
를 실제 프로젝트에 적용하면서 고려했던 내용들을 몇 가지 소개하고자합니다.
Diffable DataSource
의 SectionIdentifierType
, ItemIdentifierType
은 모두 Hashable
이어야 합니다. Hashable
은 Generic Type
을 사용하기 때문에 Protocol
이 Hashable
을 따를 경우 해당 Protocol
은 타입으로 사용할 수 없습니다. 이 때문에 SectionIdentifierType
, ItemIdentifierType
은 Hashable
타입을 따를 수 있는 Concrete Type
의 모델을 따로 구성해주어야 합니다.
예를 들어서, 다양한 ViewModel을 사용하는 ViewController는 ViewModel을 Protocol로 정의하여 사용하는 경우가 많습니다.
protocol ViewModelable {
}
class ViewController: UIViewController {
var viewModel: ViewModelable
}
그런데 Diffable DataSource
를 적용하기 위해서는 해당 Protocol이 Hashable
이어야 하는데 이 경우 ViewModelable
을 타입으로 사용할 수가 없습니다.
protocol ViewModelable: Hashable {
}
class ViewController: UIViewController {
var viewModel: ViewModelable // compile error
}
이 문제를 해결하기 위한 방법은 해당 Protocol
을 associated Value로 가지는 enum을 정의하는 것입니다.
enum ViewModel: Hasable {
case normal(viewModel: ViewModelable)
case error(viewModel: ViewModelable)
}
class ViewController: UIViewController {
var viewModel: ViewModel
}
이와 같이 처리하게 되면, 해당 ViewModel에 다양한 타입을 추가하면서도 Diffable DataSource
를 적용할 수 있게 됩니다.
또한, 기존 모델이 Concrete type
인 경우에도 문제가 생길 수 있습니다. Hashable
은 Equatable
을 따르고 있는데, 이 Equatable
을 위해 ==(lhs:rhs:)
을 구현하면서 상당히 많은 boilerplate 코드를 작성해야 합니다. 이를 회피하기 위해 기존 모델에 UUID
를 주입하는 방식을 통해 Hashable
을 따르는 모델을 쉽게 구성할 수 있습니다.
protocol UUIDHashable: Hashable {
var uuid: UUID { get set }
}
extension UUIDHashable {
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
}
데이터가 많고, UI 업데이트가 반복적으로 자주 발생하는 경우에 새로운 Snapshot을 새로 생성해서 사용할 경우 성능이 떨어지는 현상이 발생합니다. 예를 들어서, 새로운 Snapshot을 0.1초마다 생성해서 적용할 경우, 스크롤이 버벅이는 현상이 나타납니다.
var snapshot = NSDiffableDataSourceSnapshot<Int, ViewModel>()
snapshot.appendSections([0])
snapshot.appendItems(viewModels, toSection: 0)
self?.diffableDataSource.apply(snapshot,
animatingDifferences: false,
completion: { [weak self] in
self?.scrollToBottom(animated: true)
})
이는 reloadData()
를 빠르게 여러번 수행할 경우 성능 저하가 발생하는 것과 유사한 이슈입니다. 따라서 이 문제를 개선하기 위해 항상 새로운 Snapshot을 만들고 적용하는 것이 아니라, Diffable DataSource
에 바인딩된 Snapshot
에 수정되는 모델만 추가, 제거하는 방식을 사용할 수 있습니다.
private func applyQueueToSnapshot(newItems: [ViewModel]) {
var snapshot = self.diffableDataSource.snapshot()
snapshot.appendItems(newItems, toSection: 0)
diffableDataSource.apply(snapshot,
animatingDifferences: false,
completion: nil)
}
}
WWDC에 언급된 것처럼 apply()
메소드는 메인 쓰레드에서 수행되지 않아도 됩니다. 다만, 다른 Queue에서 업데이트를 수행할 때에는 apply()
메소드가 명시적으로 같은 Queue에서 호출되어야 합니다. 여기서 명시적이라는 말의 의미는 apply()
가 로직상으로 같은 Queue에서 호출되는 것으로 예상할 수 있더라도, apply()
수행을 명시적으로 Queue로 감싸주어야 하는 것을 의미합니다. 이를 처리해주지 않으면 warning이 발생하고, 예상하지 못 한 상황에 크래시가 발생할 수도 있습니다.
또한, 한 번 다른 Queue에서 apply()
가 수행된 경우에는 이를 Main Queue에서 업데이트해선 안됩니다. 즉, Queue를 바꿔가면서 apply()
를 수행해서는 안됩니다.
let queue = DispatchQueue(label: "Update Queue")
// applyQueueToSnapshot()이 queue에서 호출되더라도 apply()는 queue로 감싸주어야 합니다.
func applyQueueToSnapshot(pushed: [ViewModel], popped: [ViewModel]) {
queue.async { [weak self] in
guard let self = self else { return }
var snapshot = self.diffableDataSource.snapshot()
snapshot.appendItems(newItems, toSection: 0)
self.diffableDataSource.apply(snapshot,
animatingDifferences: false,
completion: nil)
}
}
}
DiffableDataSource로 안전하게 UIKit List 업데이트하기
Diffable Data Source
는UICollectionView
와UITableView
의 DataSource 업데이트를 좀 더 안전하고, 편리하게 수행할 수 있도록 만들어진 API입니다. 이 글에서는Diffable Data Source
는 iOS 13부터 지원되는 WWDC 주요 내용을 소개하고, 이를 적용한 후기에 대해 정리해보았습니다.WWDC - Advances in UI Data Sources
Current state-of-the-art
UICollectionView
,UITableView
를 구성하기 위해서 개발자는 아래와 같은 DataSource 코드를 작성해야 했습니다.UI를 업데이트하고자 할 때에는
reloadData()
나performBatchUpdates()
사용해야 합니다. 전체 데이터를 갱신하는reloadData()
는 작은 데이터를 갱신할 때에는 유용하지만, 보여주는 데이터의 양이 많아지거나 일부 Cell만 업데이트하고자 할 때에는performBatchUpdates()
를 통해 개별 Section, 혹은 Item을 업데이트해주어야 합니다. 하지만,performBatchUpdates()
업데이트가 잘못 되었을 때 아래와 같은 크래시를 매우 자주 일으킵니다.Diffable DataSource
Diffable DataSource
는 UI의 업데이트를 간단하고, 에러 없이 수행할 수 있도록 하기 위해 iOS 13에서 새롭게 소개된 API입니다.Diffable DataSource
는 Cell의 insert, delete 등의 동작을 사용자가 직접 수행하지 않도록 합니다. 그리고, 데이터의 업데이트를 Snapshot을 적용(apply)하는 방식으로 수행하도록 합니다.Snapshots
Snapshot
은 UI의 상태를 가지고 있는 객체로Snapshot
을 통해 데이터 업데이트시IndexPath
접근 없이 데이터를 업데이트 할 수 있습니다.UI 업데이트가 필요할 경우 새로운
Snapshot
을 만들거나, 기존에 반영된Snapshot
을 가져와서 DataSource에apply()
해주면 됩니다.How to Use
1. Diffable DataSource property 정의
UIKit에서는
UICollectionView
,UITableView
에 맞는 Diffable DataSource를 정의하여 다음과 같이 제공합니다.SectionIdentifierType
,ItemIdentifierType
과 관련해서 유의해야 할점은 해당 타입이 모두Hashable
이어야 한다는 점입니다.Diffable DataSource
는 위처럼 사용할 수 있습니다. 여기서CellProvider
의cellProvider
클로저는 기존의cellForItemAt()
의 로직을 대체하는 형태로 사용할 수 있습니다.UITableViewDiffableDataSource
는UITableViewDataSource
를 따르고 있기 때문에 이를 사용하게 되면 기존의UITableViewDataSource
는 호출되지 않습니다.2. 새로운 Snapshot 적용하기
위처럼 정의한 DataSource에 새로운 데이터를 추가하고 싶은 경우, 아래처럼
NSDiffableDataSourceSnapshot
을 만들고 모델을 추가해주면 됩니다.유의사항 및 추가 내용
Diffable Data Source
를 적용한 시점부터는performBatchUpdates()
,insertItems()
,deleteItems()
등의 API를 사용하면 안 됩니다.Snapshot
은 새로운 인스턴스 혹은 기존Diffable Data Source
의Snapshot
을 통해 생성할 수 있습니다.Snapshot
을 통해 수행됩니다. 그에 따라Snapshot
은 모델을 업데이트하는 API를 제공합니다.성능
Diffable Data Source
에서 사용하는 Diff 알고리즘은 빠릅니다.apply()
메소드는 기존 reload API들과 다르게 항상 메인 쓰레드에서 수행되지 않아도 됩니다. 하지만, 항상 동일한 Queue에서apply()
가 호출되어야 합니다.