티스토리 뷰

공부

Diffable Datasource

Zedd0202 2021. 4. 6. 15:45
반응형

안녕하세요 :) Zedd입니다.

이거 꼭 공부해보고싶었는데!

 

# Introducing Diffable Data Source

WWDC19. Apple이 Diffable Datasource를 소개합니다. 

물론 iOS 13부터 사용이 가능한 ^^..

 

Datasource하니까 가장 먼저 떠오르는게 있지 않나요? 

UITableViewDataSource

UICollectionViewDataSource

 

저는 이 2개가 떠오르는데,  Diffable Datasource에는 어떤것이 있을까요?

네! 똑같이

UITableViewDiffableDataSource

UICollectionViewDiffableDataSource

가 있습니다.

 

🙋 : UITableViewDataSource를 conform하는 대신 UITableViewDiffableDataSource를 confom하면 뭔가가 달라지겠구나!

🧑‍💻 : UITableViewDataSource는 Protocol이라 보통 UIViewController가 이를 conform하곤 했었습니다.
하지만 UITableViewDiffableDataSource는 Protocol이 아니라 Generic Class이며,
심지어 UITableViewDiffableDataSource가 UITableViewDataSource를 conform하고 있습니다

(CollectionView도 마찬가지입니다)

 

# Why

그럼 왜 Apple은 Diffable DataSource를 갑자기 만든걸까요?

이는 Advances in UI Data Sources에서 잘 설명하고 있습니다.

자..우리가 CollectionView를 구성한다고 칩시다.

UICollectionViewDataSource를 conform할거고,  

위와같은 코드들을 작성하겠죠.

너무나도 익숙한 코드들이죠? 꽤나 간단하여 빠르게 작성이 가능하고, 유연한구조입니다.

보통 Controller가 DataSource를 지원하게 됩니다. 

만약 Controller가 웹 서비스 요청을 받고, UI에게 내가 바뀌었다고 말하죠.

 

~ 결과 ~ 

자 이런 에러를 받으면, 

🧑‍💻 : 하이고...~~~....!~~!@..그래..뭐가 잘못됐니 또 

(코드를 본다)

🧑‍💻 : (모르겠음) 칫.."그것"을 사용할 수 밖에 없나...

 

🧑‍💻 : "reloadData"

 

WWDC에서도 이를 괜찮다고 말합니다. 

다만 reloadData를 하게 되면 애니메이션되지 않은 효과가 나타납니다. → 사용자 경험 저하 

 

# 뭐가 문제야?

가장 큰 문제는 DataSource역할을 하는 Data Controller가

시간이 지남에 따라 변하는 자기 자신만의 버전인 truth를 가지고 있다는겁니다. (own version of the truth)

그리고 UI역시 truth를 가지고 있습니다.

이 truth들끼리 서로 맞지 않게 되면

위와같은 에러가 나는것이죠.

이러한 접근방식은 오류가 발생하기 쉽습니다.

centralize된 truth가 없기 때문입니다.

 

# A New Approach

그래서 Apple은 완전히 새로운 접근방식을 도입합니다.

그것이 Diffable DataSource입니다.

 

# Diffable DataSource

Diffable DataSource에는 performBatchUpdates따위것들이 없습니다.

Crash나 번거로움, 복잡성, 처리하고 싶지 않은 모든 것들이 없고, apply라는 단일 메소드가 있습니다. 

 

# Snapshot

Snapshot이라는 개념도 도입됩니다. Snapshot은 간단히 말해서 현재 UI state의 truth입니다.

section과 item에 대해 Unique identifiers가 있으며, IndexPath가 아니라 이 Unique identifiers로 업데이트 하게 됩니다. 

 

아직 감이 안오실텐데요, 예제를 통해 보도록합시다.

FOO, BAR, BIF가 있고, Controller가 변경되었다고 가정해봅시다. 

즉, Apply할 수 있는 새로운 snapshot이 생긴거죠. 

Apply만 하게 되면 새로운 Snapshot이 적용되게 됩니다.

 

# 사용해보기 

WWDC 예제 앱을 똑같이 따라서 만들어봤어요.

CollectionView를 사용한건데, TableView도 거의 똑같다고 보시면 될 것 같아요.

 

순서는 아래와 같습니다. 

1. Connect a diffable data source to your collection view.
2. Implement a cell provider to configure your collection view's cells.
3. Generate the current state of the data.
4. Display the data in the UI.

 

1. Connect a diffable data source to your collection view.

DiffableDataSource를 우리의 CollectionView와 연결하려면 일단 DiffableDataSource를 만들어야겠죠? 

DiffableDataSource는 Protocol같은게 아니라 Generic Class라고 그랬습니다.

정의가 

이런데, Generic class이므로 적절한 타입을 넣어서 생성하면 됩니다.

SectionIdentifierType과 ItemIdentifierType을 잘 보셔야 하는데요. 둘 다 Hashable을 준수하는 타입만 들어갈 수 있습니다.

Swift의 Primitive Type들은 다 들어간다고 보면 됩니다.

 

UICollectionViewDiffableDataSource를 만들려고 하면 

먼저 이렇게 보여질것입니다. 저는 Generic type을 먼저 지정해줄게요. 

SectionIdentifierType과 ItemIdentifierType을 넣어주면 됩니다. 다시 한번 말하지만..Hashable을 준수하는 타입만 들어갈 수 있습니다. 

 

[SectionIdentifierType]

SectionIdentifierType은 보통 Int를 넣어주면 될텐데..

enum Section: CaseIterable {
    case main
}

저는 Section enum을 만들어서 넣어주겠습니다.

🙋 : Hashable준수해야한다면서..?

🧑‍💻 : enum은 모든 case, associated value가 Hashable을 준수하면 자동으로 synthesise됩니다. 

 

[ItemIdentifierType] 

저는 보여줄 item이 단순한 String이므로 그냥 String으로 넣어주겠습니다.

⚠️ 그냥 String으로 넣어줄 때, 같은 Value가 중복으로 들어가있으면 crash가 발생합니다. 이 점 꼭 주의해주세요!

중복된 value에 대한 처리를 해주고싶다면 이 글을 확인해주세요! 

custom type을 넣어보는건 다음에 해볼게요! (custom type이 들어간 코드는 Apple 예제에서 확인할 수 있습니다.)

enum Section: CaseIterable {
    case main
}

UICollectionViewDiffableDataSource<Section, String>

 

자 이제, collectionView를 연결해주는 작업과 Provider를 구성작업이 남았습니다. 

collectionView에는 우리의 collectionView를 넣어주면 되고,

cellProvider는 3개의 파라미터를 제공합니다.

collectionView, IndexPath, ItemIdentifierType입니다.

UICollectionViewDiffableDataSource<Section, String>(collectionView: self.collectionView) { (collectionView, indexPath, dj) -> UICollectionViewCell? in
        // code
}

최종 코드!

(ItemIdentifierType은 앞에서 Generic Type으로 String을 지정해줬기 때문에 String으로 나오는 것입니다.)

 

그리고 추가작업인데, 이 dataSource를 코드 다른곳에서도 사용해야하기 때문에

var dataSource: UICollectionViewDiffableDataSource<Section, String>!


self.dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: self.collectionView) { (collectionView, indexPath, dj: String) -> UICollectionViewCell? in
        //
}

이렇게 dataSource전역변수를 만들어서 관리해줍니다.

 

2. Implement a cell provider to configure your collection view's cells.

여기서부터는 익숙한 작업이 나옵니다.

cell을 만들고, cell에 데이터를 넣어주는 것이죠.

self.dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: self.collectionView) { (collectionView, indexPath, dj) -> UICollectionViewCell? in
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? DJCollectionViewCell else { preconditionFailure() }
    cell.configure(text: dj)
    return cell
}

이렇게요. (물론 이렇게 하기 전에, collectionView에 해당 cell이 register된 상태여야합니다.) 

 

3. Generate the current state of the data.

아까 Snapshot이야기를 했었는데요,

우리의 SearchBar에 Text가 변경되면 그때마다 새로운 Snapshot이 만들어져야합니다.

그래서 그걸 Apply해서 UI를 업데이트 해야하니까요!

func performQuery(with filter: String?) {
    let filtered = self.arr.filter { $0.hasPrefix(filter ?? "") }   // 1

    var snapshot = NSDiffableDataSourceSnapshot<Section, String>()  // 2
    snapshot.appendSections([.main])                                // 3
    snapshot.appendItems(filtered)                                  // 4
    self.dataSource.apply(snapshot, animatingDifferences: true)     // 5
}

1. SearchBar가 변경될 때마다 CollectionView에 보여줘야하는 데이터가 달라집니다. 저는 prefix로 시작하는 것들을 filter해줬습니다.

2. Snapshot을 만듭니다. NSDiffableDataSourceSnapshot은 View에서 특정 시점의 데이터 상태를 나타내는 친구입니다. 지금은 "빈 Snapshot"을 만들어줬습니다.

3, 4. snapshot는 section과 item으로 구성되는데요. section 및 item을 추가, 삭제, 이동하여 표시할 내용을 구성하면 됩니다.

Section에는 

enum Section: CaseIterable {
    case main
}

main을 넣어주고,

Items에는 filter한 데이터를 넣어줍니다.

 

4. Display the data in the UI.

저~~기 위에서 말했듯이, 다 필요없고 Diffable DataSource에서는 Apply만 하면 끝입니다.

이쪽에서 dataSource를 사용해야하므로 아까 전역으로 선언해준거고,

우리가 configure한 snapshot과 함께 Apply를 호출해줍니다.

왼쪽 gif를 보면 애니메이션이 예쁘게 되는 것을 볼 수 있는데, animatingDifferences를 true로 줘서 그렇습니다.

false로 주면 오른쪽 gif처럼 나오게 됩니다. 

 

이렇게만 하면 끝나게 됩니다!

프로젝트는 github에 올려놓았습니다. 

 

+ 추가

self.dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: self.collectionView) { (collectionView, indexPath, dj) -> UICollectionViewCell? in
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? DJCollectionViewCell else { preconditionFailure() }
    cell.configure(text: dj)
    return cell
}

아까 이렇게 Provider를 구성했는데요. 스토리보드에 cell이 있으면 모르겠지만..xib나 코드로 만들어진 collectionView같은경우에는 반드시 register를 해줘야합니다. 

self.collectionView.register(DJCollectionViewCell.self, forCellWithReuseIdentifier: "cell")

이렇게요.

이렇게 register를 따로 안하고, Diffable Datasource만들 때 register & configure작업을 하게 할수 있습니다. 

let cellRegistration = UICollectionView.CellRegistration<DJCollectionViewCell, String> { (cell, indexPath, dj) in
    cell.configure(text: dj)
}

이렇게 cellRegistration을 만들고

self.dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: self.collectionView) {
    (collectionView, indexPath, dj) -> UICollectionViewCell? in
    return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: dj)
}

dequeueConfiguredReusableCell을 사용하여 cell을 만들어주면 됩니다. 

프로젝트에 Way 2로 주석처리 해놓았으니 참고하세요. 

 

참고

developer.apple.com/videos/play/wwdc2019/220

developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

 

반응형