Do.

Diffable Data Source 본문

iOS

Diffable Data Source

Hey_Hen 2022. 2. 18. 23:30

(safari에서는 gif를 이미지를 불러올 수 없습니다. 곰탱이로 보여집니다. chrome로 봐요)

Intro

DiffableDataSource는 wwdc2019에서 Advances in UI Data Sources라는 이름으로 발표된 API입니다.
DiffableDataSource는 기존 DataSource를 구성하는 새로운 방법으로, 복잡한 코드를 간편하고 빠르게 작성할 수 있게 해줍니다.
CollectionView와 TableView에서 애니메이션 효과는 꼭 필요한 유저 경험을 제공합니다.

출현배경

CollectionView, TableView 모두 동시에 적용 되므로, CollectionView로 앞으로는 통일하겠습니다.
간단한 CollectionView 써보셨다면, 사실 크게 복잡하지 않은 앱에서는 딱히 문제가 없으셨을 것입니다.
셀을 삭제하고, 추가할 때

collectionView.deleteItems 또는 insertItems을 통하면 애니메이션 효과를 얻을 수 있습니다.

HIG에 따르면 애니메이션은 사용자에게 조작감을 향상 시키고, 앱을 컨트롤하고 있다는 느낌을 들게해주는 중요한 요소입니다.
앱이 조금 복잡해져
Item을 삭제, 추가, 이동 한다고 했을 때는 batch block을 열어, 안에서 IndexPath를 계산해주어
앞서 애니메이션을 주는 메서드들을 작성할 수 있습니다.
그런데 앱은 이것보다 더 복잡해질 수 있습니다.

이처럼 대량의 데이터가 오갈 수 있는 외부 서비스나, DB , 혹은 아까 말했던 것처럼 좀더 복잡하게 동작하는 DataSource의 경우 한번에 데이터가 여러개 추가, 삭제, 편집 등 다양하게 동작할 수 있습니다.
유저가 인터페이스를 통해서 조작하는거 까지는 처리가 쉬운데, 외부에서 들어오는 경우, batch 업데이트를 어떻게 해야 할까요?
방법이 없는 것은 아니지만, 꽤 코드가 복잡하고 길어질 것은 확실합니다.
여기서 한번쯤은 다들 경험 해보셨을

batch update 크래시 에러를 볼 수도 있습니다.

앱이 크레시가 발생하기 때문에 여간 위험한게 아니기 때문에 애니메이션 없이 reloadData()reloadSection()을 한다고 가정해봅시다.

임의의 앱을 만들어 보았는데요
개발자가 이력서 보이기를 설정하면, 화면에 표시되는 구조입니다.(가상으로)
애니메이션 없이 처리되니까 뭐가 어떻게 변하고 있는지, 알 수가 없고 꽤 정신 없습니다.
이렇게 경우에 따라 애니메이션이 없는 앱은 나쁜 유저 경험을 발생시킵니다.
이를 해결하는 것이 바로 Diffable Data Source 인것입니다.

Diffable Data Source

Diffable Data Source가 위에서 말한 어떠한 문제점들을 해결할까요?
1. No more batch update
2. No more IndexPath
3. Easy Animation
Diffable Data Source는 diff 연산과, View 업데이트의 두 가지고 생각할 수 있습니다.

DiffableDataSource는 현재 뷰의 상태를 Current Snapshot이라는 형태로 관리를 하게 됩니다.

그리고 아까처럼 데이터의 변동 사항이 생기면 이를 New Snapshot으로 봅니다.

New Snapshot을 생성하고 이를 Current Snapshot으로 적용하기 위해 apply() 메서드를 호출하게 되는게 이게 DiffableDataSource에서 가장 매력적인 부분입니다.

apply()메서드는 원래 Data Source에서 우리가 일일이 배치 블럭 열고, 인덱스 패스 계산하고, deleteRows등을 호출 하는 과정을 한번에 해결해줍니다.

DiffableDataSource를 사용한 상태입니다. 이 모든게 DiffableDataSource를 적용하고, apply() 메서드만 호출한 결과물인거죠.
그러면 DiffableDataSource 는 어떻게 사용할 수 있나요?
아래 3가지를 구현하면 됩니다.
1. Hashable
2. configureDataSource()
3. configureSnapshot()
하나씩 천천히 봅시다.

Hashable

DiffableDataSource에 들어가는 인스턴스는 Hashable을 채용한 모델이여야 합니다. Diff 연산을 하기위해, 고유한 ID를 가질 필요가 있는 것이죠

final class Developer: Hashable {
  static func == (lhs: Developer, rhs: Developer) -> Bool {
    lhs.id == rhs.id
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }

  var id = UUID().uuidString
  var name: String
  var major: Major
  var language: [Language]
  var carrer: String
  var searcable: Bool = true

    ...
}

Configure Data Source

ConfigureDataSource는 특별한게 아니고, 기존 DataSource에서 하던 cellForRowAt 같은 것이라고 생각하시면 됩니다.
RxDataSource를 사용해셨던 분들은 구조가 한번에 이해가 되실 겁니다.
우선 클래스의 Property에 DataSource 선언이 필요합니다.

var dataSource: UICollectionViewDiffableDataSource<Section, Developer>!

UICollectionViewDiffableDataSource(TableView도 UITableViewDiffableDataSource로 똑같습니다.)

이는 제네릭 타입 클래스로 Section Hashable Type과 Item Hashable Type을 받습니다. Item Hashable Type은 앞서 Hashable을 적용한 모델이 들어가면 되는것이고, Section에는 임의로 만드시면 됩니다.
예를들어 보여주고자 하는 CollectionView에 Main섹션, Body섹션 이 있다면

enum Section {
  case main
  case body
}

이런식으로 만든 후 Section Type에 전달해 주면 됩니다. 어렵지 않죠?

이제 configureDataSource()를 만들어서 호출해보겠습니다.

private func configureDataSource() {
    dataSource = UICollectionViewDiffableDataSource<Section, Developer>(collectionView: collectionView) { collectionView, indexPath, developer in
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCardCell.identifier, for: indexPath) as! ProfileCardCell
      cell.configure(developer)
      return cell
    }
  }

메서드는 viewDidLoad() 등에서 호출해주시면 됩니다.

UICollectionViewDiffableDataSource는 초기화로 collectionViewcellProvider 클로져를 받습니다.

cellProvider를 보면 collectionView, indexPath, itemIdentifier 가 전달되는데

itemIdentifier는 제네릭으로 전달한 Hashable 모델입니다. 그러니 지금은 developer를 그대로 전달받는 것이죠

그래서 원래 데이터 리스트에서 indexPath로 찾을 필요 없이 바로 cell에 전달하시면 되는겁니다.
그런데 내용을 기존 data Source의 cellForRowAt 와 똑같죠? 심지어 numberOfRowInSection, NumberOfSection 는 호출하지 않아도 됩니다.

Configure Snapshot

이제 dataSource와 cell구성은 되었으니 snapshot구성을 해봅시다. snapshot도 마찬가지로, viewDidLoad()등에서 호출하는데, configureDataSource()다음으로 호출해주시면 됩니다.

func configureSnapshot(animatingDifferences: Bool = true) {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Developer>()
    snapshot.appendSections([.main])
    snapshot.appendItems(storage.filteredDevelopers, toSection: .main)
    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
  }

snapshot 구성은 NSDIffableDataSourceSnapshot 클래스를 이용합니다.

인스턴스를 생성하고, append Sectionappend Item을 전달해 준뒤

dataSource.apply() 해주시면 끝입니다.

body 섹션까지 전달하고 싶다면 appendSections[.main, .body] 와 같은 형태로 작성하면 되겠죠?

이게 끝입니다. 앞으로 데이터가 변경되면 configureSnapshot()만 호출해주시면, 알아서 CollectionView가 애니메이션 과 함께 동작할 것입니다.

아까의 화면이 지금까지 한 것의 결과물인거죠. 데이터 변경 시점만 Notification이나 Rx등을 통해 옵저버를 통해 구독하고 있다가 configureSnapshot() 코드만 호출하면 끝인겁니다.

Performance

이렇게 쉬운 DiffableDataSource 성능은 어떨까요?
diff 연산은 결국 데이터를 하나씩 비교하는 작업이기 때문에 O(N)의 시간 복잡도를 가집니다. 그러면 데이터가 커지면, 일일이 백그라운드 큐에서 apply()를 해야하나요?
아니요!
DiffableDataSource는 diff연산은 알아서 백그라운드 큐에서, UI update는 알아서 메인 큐에서 실행해줍니다. 맞습니다. 스레드 관리가 필요가 없습니다!

또 기존 indexPath로 Item을 얻고, 원래 데이터에서 해당 아이템을 찾고 하는 과정을 dataSource.itemIdentifier(for:)메서드를 통해 O(1) 시간으로 얻어올 수 있습니다. 모델이 Hashable이기 때문입니다. 멋지죠?

Trade-off

이렇게 좋은 DiffableDataSource 문제는 없을까요?
아쉽게도 있습니다.

1. iOS 13.0이 최소버전
WWDC2019 (iOS 13.0)으로 발표되었기 때문에 미만의 OS에서는 사용할 수가 없습니다.
출시한 앱에 적용해보았는데, OS가 13.0, 14.0, 15.0에서 똑같은 코드로 각기다른 에러가 발생하더라구요, 제가 잘 사용하지 못한 것도 있지만, 바르게 사용하기 위해서는 숙달하는 시간도 필요할 것 같습니다.

2. RxDataSource
프로젝트에서 RxSwift를 사용한다면 RxDataSource라는 대체제도 있습니다. RxDataSource도 한계점이 있지만, 굳이 RxDataSource부분을 DiffableDataSource로 바꿀래? 라고 그러면 거기까지는 아니라는 거죠

3. Bug
DiffableDataSource를 쓰면 원래 dataSource = self가 완전히 무력화 되는데,
제가 겪었던 버그는 tableView에 Drop, Drag Delegate를 사용해야 하는데 전혀 호출되지 않았던 적이 있습니다.

https://gookbobhenry.notion.site/DiffableDataSource-DropDelegate-ISSUE-52d6e52640bb41cf8172c58d490fe37b

DiffableDataSource DropDelegate ISSUE

TableView에서 Drag, Drop Delegate를 이용해서 드래그하여 셀 재정렬이 가능한데, DiffableDataSource에서 Drop Delegate 메서드들이 호출이 안 되던 문제.

gookbobhenry.notion.site


delegate = self 순서 변경으로 해결된 문제였지만, 얼마나 삽질했는지 생각하면 지금도 화가납니다.

그럼에도, 기존 DataSource에 비해 애니메이션 까지 구현하기가 굉장히 쉽고, 편리합니다. 그래서 저는 간단한 앱에도 DiffableDataSource를 씁니다. 이쪽이 작성이 더 빠르거든요.

Next

DiffableDataSource는 iOS 14.0에서 한단계 더 진화하는데 이부분은 아직 저도 공부중으로, 관심있으시면 WWDC2020에 Advances in diffable data sources 영상을 보시는 걸 추천드립니다.
Section Snapshot과 Reordering Support 가 새로 생기거든요

References / Resources

WWDC 2019 Advances in UI Data Sources
Reference Project

Sample Project

'iOS' 카테고리의 다른 글

Swift- CoreBluetooth 찍먹  (0) 2022.03.01
NMapsMap M1 Build Error  (0) 2022.02.26
Design Pattern - Coordinator Part1  (0) 2022.02.09
Access Control  (0) 2022.02.09
SwiftGen 사용기 (Homebrew)  (0) 2022.02.09
Comments