TableView(또는 CollectionView)를 그리기 위한 데이터를 관리하고 UI를 업데이트 하는 역할을 한다. Data Source와 달리 데이터가 달라진 부분을 추적하여 자연스럽게 UI를 업데이트한다.
간단히 말하자면, DataSource란 TableView를 나타낼 데이터를 구성하고 UI를 업데이트 하는 역할을 한다. (이 글 참고) 기본적으로, Diffable Data Source와 DataSource의 역할은 같다.
그러나, Diffable Data Sources를 사용하면 tableView나 collectionView를 단순하게 업데이트가 가능하다. 이전 테이블과 달라진 부분을 자동으로 알아차리고, 새로운 부분만 다시 그리기 때문이다.
identifier
와 snapshot
을 사용하는 간소화 된 Data 모델을 정의 하고, 이를 이용하여 UI를 업데이트 한다.위와 같은 효과를 어떻게 얻게 되는지 조금 더 자세히 알아보려고 한다.
기존 tableView나 collectionView를 사용하기 위해서는 프로토콜의 세가지 메소드를 반드시 구현해 주어야 한다.
위와 같은 코드로 collectionview를 무리없이 사용할 수 있다.
그럼, 이전까지 잘 사용해 왔는데 왜 datasource가 필요한 걸까?
보통 tableView의 데이터를 구성하기 위해 Controller에게 numberOfSection
을 물어본다. controller는 데이터를 채우기 위해서 API통신을 요청하고 응답을 받으면 tableView에게 변화를 알린다.
Diffable Data Source는 바로 다음 상황을 해결하기 위한 것이다.
섹션 수가 잘못되어 앱이 종료되는 경우이다. 업데이트 되기 전과 후의 section 수가 하나 이상 차이난다는 말인가? 잘 모르겠다
어쨌든, 해당 에러는 데이터의 변경 상황을 수동으로 관리하고 동기화 해야함을 말하고 있다. 따라서 데이터를 동기화 해주는tableView.reloadData()
를 이용하여 해결 할 수 있다.
그렇다면, 다시 질문을 갖게 된다.
reloadData()
로 해결하는 것과 Diffable Data Source로 해결하는 것의 차이는 사용자 경험(UX) 에 있다. reloadData()
의 경우 TableView를 한번에 업데이트 하므로 뚝 뚝 끊어지는 UI를 보게 된다. 반면, Diffable Data Source를 사용하는 경우 변경된 데이터 부분에 대하여 스르륵 사라지고 추가되는 UI 효과가 적용된다.
앞서 Diffable Data Source를 소개 할 때 "자연스럽게 UI를 업데이트 한다." 라고 설명한 이유이다.
DataSource | DiffableDataSource |
---|---|
결론부터 말하자면 Hash value
를 사용한다.
Diffable Data Source를 사용하기 위해서 두가지 generic type을 가진다.
이 두 제네릭 변수는 반드시 Hashable
해야한다.
class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject
where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>
where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
Diffable Data Source는 SectionIdentifierType
, ItenIdentifierType
두개의 generic parameter로 결정된다. 이 두 파라미터는 반드시 Hashable
해야 한다.
apply시에 각 hash value를 비교하여 추가 or 삭제된 부분을 인지하게 된다.
ex) snapshot 생성 및 적용
var snapshot = NSDiffableDataSourceSnapshot<Int, MyModel>()
snapshot.appendSections(storage.sections)
for section in storage.sections {
snapshot.appendItems(storage.modelsForSection(section), toSection: section)
}
datasource.apply(snapshot)
다음의 예를 보자.
let itemsBefore = [{ "name": "Donny Wals" }, { "name": "Donny Wals" }]
let itemsAfter = [{ "name": "Donny Wals" }, { "name": "Donny Wals" }]
변화되기 이전의 item과 변화 된 이후에 item이다. 눈으로 보기는 동일하지만, 내부의 두 객체는 순서가 바뀌었을 수도 있다. 확인이 불가능 하지만, 각 객체는 자신의 entity를 가진다.
itemsBefore == itemsAfter // True
itemsBefore.hashValue == itemsAfter.hashValue // True
하지만 Diffable Data Source는 이 둘을 같다고 볼 것 이다. name은 객체를 식별할 수 있는 고유 속성이 아니기 때문이다.
따라서, setion과 item의 identifier는 반드시 unique해야하며 hashable 프로토콜을 따라야 한다.
UUID나 back-end에서 제공하는 identifier를 이용한다면 위와 같은 경우를 막을 수 있다.
ex)
struct Mountain: Hashable {
let name: String
let height: Int
let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
}
O(N) (N: the number of item)
모든 아이템의 hash 값을 비교해 보아야 하므로 선형 탐색에 걸리는 시간과 동일하다.
data source의 apply작업이 오래걸린다면, background queue에서 수행하면 된다. background queue에서 이뤄지더라도 안전성을 보장할 수 있기 때문이다. frmaework는 diffable을 계산이 완료되면 메인 큐로 돌아가서 결과를 적용하게 된다.
좋은 글 감사합니다! 글 깔끔하게 작성하시는것 같아요