[WWDC] Advances in UI Data Sources
https://developer.apple.com/videos/play/wwdc2019/220
Advances in UI Data Sources - WWDC19 - Videos - Apple Developer
Use UI Data Sources to simplify updating your table view and collection view items using automatic diffing. High fidelity, quality...
developer.apple.com
Current State-of-the-Art
오늘의 주인공을 알아보기 전,
기존의 콜렉션뷰를 먼저 살펴봅시다.
기존의 콜렉션뷰는
섹션 수, 각 섹션의 항목수를 지정하고, 렌더링하면서 필요한 셀을 요청하는데요,
지난 10년간 사용되어오던 방식입니다.
기존의 콜렉션뷰에서 1, 2차원의 배열 모양 만들 때는 큰 문제가 없지만
실제 앱은 1, 2차원 배열보다는 더 복잡합니다.
그리고 이렇게 복잡한 화면을 구성할 때,
데이터 소스들을 지원하는 컨트롤러 또한 복잡하겠죠?
예를 들어서 Core Data랑 상호작용하거나,
웹 서비스와 통신하거나, 다양한 처리들을 담당하는데요.
그래서 컨트롤러가 할 일이 엄청 많습니다.
간단한 예시를 살펴보면요.
이렇게 UI가 "이 섹션에 몇 개의 항목이 있나요?" 라고 물으면,
Controller가 이에 대답해주는 매우 직관적인 구조입니다.
그런데, 만약 웹 서비스와의 통신이 결합된다면 어덜까요?
웹 서비스에서 요청이 들어오면, Controller는 UI에게 "데이터가 변경되었어요"라고 알리고
UI는 이를 감지해 뷰를 변경하겠죠.
UITableView와 UICollectionView에서는
이 변화를 업데이트하기 위해 배치 업데이트(batch update)를 수행하게 됩니다.
batch update를 수행하는 과정이 매우 복잡하고,
제대로 처리하지 않으면 아래와 같은 문제들을 마주하는데요.
이런 문제들을 다들 겪어보셨나요?
이 문제를 해결하려다가 결국 지쳐서 reloadData()를 호출하게 되는데,
(뜨끔)
reloadData()가 잘못된 방식은 아니지만
UI가 애니메이션 없이 갱신되기 때문에 사용자 경험이 저하됩니다.
그래서 문제의 본질이 무엇인가요?
Where Is Our truth?
진실(truth)이 어디에 있는가?
강연에서는 기존의 문제가
No centralized truth(중앙 집중화된 truth가 없음)
때문에 발생했다고 설명됩니다.
즉 Controller는 자체적인 truth를 가지고 있고, UI도 또 다른 truth를 가지고 있는데
UI 레이어 코드는 이 두 가지가 동기화되도록 항상 유지를 해야 하는 것입니다.
Controller와 UI 간의 동기화
가 어렵기 때문에, 현재의 접근 방식에서 오류가 발생하기 쉬운 것입니다.
A New Approach: DiffableDataSource
그래서 나타난 것이 DiffableDataSource 입니다.
이제 performBatchUpdates가 필요하지 않기에,
이에 따른 충돌, 문제, 복잡성 들이 모두 사라집니다.
대신 이제 apply만 호출하면 되는데요.
apply는 단순하고, 자동적으로 변경 사항을 적용합니다.
여기에서 핵심적으로 등장하는 개념이
Snapshot 입니다.
Snapshot은 현재 UI 상태의 진실(truth),
즉 현재 상태를 캡처하듯 나타내는 단순한 개념인데요.
기존의 IndexPath 대신,
고유한 섹션 식별자(section identifiers)와 항목 식별자(item identifiers)를 사용합니다.
Snapshot의 작동 방식
예를 들어, 화면에 FOO, BAR, BIF라는 항목이 있다고 가정해봅시다.
여기에서 컨트롤러의 데이터가 변경되어 새로운 Snapshot이 생성되었는데요.
새로운 Snapshot에는 BAR, FOO, BAZ가 포함되어 있고,
구성요소들의 순서가 바뀌었습니다.
Apply 메서드는 현재 상태와 새로운 상태를 비교하여
변경 사항을 UI에 적용합니다.
다른 작업 없이 apply만 호출하면 됩니다.
Diffable Data Source
Diffable Data Source에는 다음과 같은 클래스들이 존재하고,
iOS, tvOS
- UICollectionViewDiffableDataSource
- UITableViewDiffableDataSource
macOS
- NSCollectionViewDiffableDataSource
현재 UI 상태를 추적하는 NSDiffableDataSourceSnapshot 클래스가 있습니다.
Example: Mountain Search
데이터를 콜렉션뷰에 반영하기 위해서는
다음의 세 가지 순서만 기억하면 됩니다.
1. Snapshot을 생성한다.
2. Snapshot을 필요한 데이터로 채운다.
3. Snapshot을 Apply하여 UI에 변경 사항을 적용한다.
DiffableDataSource는 내부적으로 Diff 연산을 수행하고,
UI에 적절한 업데이트를 자동으로 반영하는데요.
첫번째 예제는 위와 같이 세계 여러 나라의 산을 검색하는 앱입니다.
검색창에 텍스트를 입력할 때마다 performQuery를 호출해
데이터를 검색하고, 업데이트합니다.
1. 검색 필터를 이용해 새로운 데이터 리스트를 가져옴
2. 새로운 Snapshot을 생성
3. Snapshot에 필요한 섹션, 항목 추가
4. Apply를 호출하여 UI를 업데이트
즉, 현재 상태를 반영한 Snapshot을 생성하고 Apply하는 것만으로
UI가 자동으로 변경되는 것인데요.
여기서 Snapshot은 제네릭 클래스이기에
Section과 Item의 타입을 직접 지정해야 합니다.
- Section 타입: enum
- Item 타입: struct이며, Hashable을 구현해야 함
enum은 기본적으로 Hashable을 구현하므로 추가 작업이 필요 없지만,
struct 타입인 Mountain은 명시적으로 Hashable을 준수해줘야 합니다.
그래서 위와 같이 Mountain을 Hashable하게 만들어주어야 합니다.
Hashable이기 때문에 각 mountain이 고유한 id 값을 기반으로 구분이 되는데요,
그래서 DiffableDataSource으로 항목을 쉽게 추적할 수 있는 것입니다.
이제 UI에 표시할 항목을 구성할 필요가 없이
Identifier만 있으면 DiffableDataSource가 알아서 처리해줍니다 ~
Example: Wi-Fi
두 번째로는 Wi-Fi 설정 화면 예제가 소개되었는데요,
설정하는 섹션과 네트워크 목록 섹션 두 가지로 나뉘어집니다.
Wi-Fi를 끄면 목록이 사라지고,
다시 켜면 네트워크 리스트가 애니메이션이 존재하는데
performBatchUpdates을 쓰지 않아도 DiffableDataSource가 자동으로 처리한다고 합니다.
- Wi-Fi가 꺼져 있을 경우, config 섹션만 추가
- Wi-Fi가 켜져 있으면, networks 섹션도 추가
기존에는 IndexPath를 직접 다뤄야 했지만, Identifier만 사용하면 됩니다.
Example: Insertion Sort Visualizer
세 번째 예제는 랜덤 색상의 블록들이 등장하고
Sort 버튼을 누르면 애니메이션과 함께 색상이 정렬됩니다.
세 번째 예제에서는 기존의 reloadData() 대신 Snapshot을 가져와서 수정하는 방식을 사용하는데요,
(dataSource.snapshot())
위와 같이 기존 스냅샷 값을 가져와서 업데이트해주면
정렬 과정이 자연스럽게 애니메이션과 함께 반영됩니다.
Considerations
위의 예제에서 보았듯이,
Diffable Data Source에서는 항상 세 가지 단계를 반복하는데요.
1. Snapshot을 생성한다.
2. Snapshot을 데이터로 채운다.
3. Snapshot을 Apply한다.
performBatchUpdates는 이제 안녕! (ㅋㅋ)
insertItems, deleteItems 같은 메서드도 더 이상 호출할 필요가 없습니다
(apply 짱)
여기에서 Snapshot을 생성하는 방법은 두 가지가 있습니다.
1. 새로운 빈 Snapshot을 생성 -> 기본적인 UI 업데이트에 많이 사용
2. 현재 상태를 기반으로 Snapshot을 가져옴 -> 점진적인 변경이 필요할 때 사용 (e.g., 컬러 블록 정렬 예제)
또 추가적으로
현재 UI 상태 파악을 용이하게 하는 메서드들이 존재합니다.
- snapshot.numberOfSections → 섹션의 개수를 가져옴
- snapshot.numberOfItems → 전체 아이템 개수를 가져옴
- snapshot.itemIdentifiers → 현재 포함된 모든 항목의 ID 목록을 가져옴
이전의 콜렉션뷰에서는 IndexPath를 기반으로 동작했지만,
DiffableDataSource에서는 Identifier를 사용합니다.
즉, 항목을 삽입하거나 삭제할 때도
IndexPath를 직접 참조할 필요 없이 고유 ID(identifier)를 사용하면 됩니다.
그리고 기존 특정 섹션에 항목을 추가할 때,
섹션을 지정할 필요 없이 마지막 섹션에 자동 추가(append)할 수도 있습니다.
여기서 중요한 점!
DiffableDataSource에서는 ID를 통해 항목을 구분하기에
항목들이 고유하게 식별할 수 있어야 하는데요.
즉, Identifier는 반드시 중복되지 않는 값이어야 합니다.
✅ 올바른 Identifier
- UUID()
- 데이터베이스에서 제공하는 고유한 ID 값
- 네트워크 응답에서 받은 특정 Key 값
❌ 잘못된 Identifier
- IndexPath.row 같은 동적인 값
- 항목의 이름 (같은 이름을 가진 항목이 있을 수 있음)
추가로 UICollectionViewDelegate는 여전히 IndexPath를 사용하는데,
여기에서 IndexPath를 Identifier로 변환하는 API를 활용할 수 있습니다.
if let identifier = dataSource.itemIdentifier(for: indexPath) { }
apply() From a Background Queue
또!
UI 업데이트는 항상 메인 큐에서 실행되어야 하잖아요?
하지만 DiffableDataSource의 Apply 메서드는
백그라운드에서 실행할 수도 있습니다.
1. apply가 백그라운드에서 실행되면, Diff 연산을 백그라운드에서 수행
2. Diff 연산이 끝난 후, 메인 큐에서 UI 업데이트를 수행
다만, 일관성을 유지하기 위해
한 번 백그라운드에서 적용하기 시작하면 계속 백그라운드에서 적용해야 합니다.
AirDrop에서도 DiffableDataSource가 사용되었다고 하는데요,
새로운 기기가 나타나거나 사라질 때도,
UI가 자동으로 애니메이션과 함께 업데이트 되는 예시가 소개되었습니다.