UITableViewDataSource, UICollectionViewDataSource에서 비교적 간단하고 직관적인 API를 전달해주는 것과 달리 지금의 앱은 점점 복잡해지고 있다.
UI data source가 controller에 의해 지원되는데 Controller는 하는 일이 점점 많아짐
Web Services
DB
이러한 복잡한 앱을 만들다 보면(예: Controller가 데이터가 바뀌었다고 UI한테 알려줘야 하는 경우) 업데이트가 잘못되었을 경우, 다음과 같은 에러를 꽤 자주 보게 된다.
1 2
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (10) must be equal to the number of sections contained in the collection view before the update (10), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted).' ***
문제를 해결하기 위해서 가장 확실한 방법은 reloadData() 를 호출하는 것이지만 이 방법은 애니메이션이 없어 유저에게 좋은 인상을 주지 못한다.
Where is Our Truth?
Data source와 현재 UI 상태는 항상 동일해야 함
현재의 방법은 에러가 일어나기 쉬운 상태
중앙 집중된 Truth가 없음
🆕 Diffable Data Source
❌ performBatchUpdates() - 크래시를 유발하고 귀찮으며 복잡함
✅ apply() - 쉽고 자동으로 diffing
Snapshots
UI 상태의 truth
섹션과 아이템의 Indexpath 대신 Unique identifier를 사용
네 가지 요소
iOS, tvOS
UICollectionViewDiffableDataSource
UITableViewDiffableDataSource
MacOS
NSCollectionViewDiffableDataSource
All Platform
NSDiffableDataSourceSnapshot
DiffableDataSource를 사용하는 세 가지 주요 과정
새로운 Data로 DataSource를 업데이트한다.
새로운 Snapshot을 만든다.
apply() 메서드를 통해 변화를 제출한다.
새 Snapshot 만드는 법
1 2 3 4 5 6 7 8 9 10
let mountains = mountainsController.filteredMountains(with: filter).sorted { $0.name < $1.name }
// NSDiffableDataSourceSnapshot은 SectionIdentifierType과 ItemIdentiferType을 가지는 제네릭 타입 let snapshot = NSDiffableDataSourceSnapshot<Section, MountainsController.Mountain>() // .main은 Section enum의 case snapshot.appendSections([.main]) // ItemIdetifierType의 배열을 전달해야 하지만 // Mountain은 해당 타입의 구현체이기 때문에 그대로 전달 snapshot.appendItems(mountains) dataSource.apply(snapshot, animatingDifferences: true)
ItemIdentiferType의 정의
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Hashable을 따르기 때문에 DiffableDatasource가 // Unique한 identifier를 가지고 // 무엇이 추가 / 삭제되어야 하는지 알 수 있음 structMountain: Hashable{ ... let identifier = UUID() funchash(into hasher: inout Hasher) { hasher.combine(identifier) } staticfunc == (lhs: Mountain, rhs: Mountain) -> Bool { return lhs.identifier == rhs.identifier } ... }