[TIL] DiffableDataSource

숑이·2023년 9월 14일
0

iOS

목록 보기
20/26
post-thumbnail

오늘은 DiffableDataSource를 사용해서 CollectionView를 구현해보도록 하겠습니다.

DiffableDataSource

공식 문서에서는 다음과 같이 설명합니다.

데이터를 관리하고, CollectionView에 셀을 제공하기 위해 사용하는 객체

DiffableDataSource는 CollectionView와 함께 작동하는 특수한 유형의 DataSource입니다. CollectionView의 데이터 및 UI 업데이트를 간단하고(?), 효율적인 방식으로 관리하는데 필요한 동작을 제공합니다. 또한, UICollectionViewDataSource 프로토콜을 준수하며 프로토콜의 모든 메서드에 대한 구현을 제공합니다.

CollectionView를 데이터로 채우려면 다음과 같이 하세요

  1. DiffableDataSource를 CollectionView에 연결합니다.
  2. Cell Provider를 구현하여 CollectionView의 Cell을 구성합니다.
  3. 데이터의 현재 상태를 생성합니다.
  4. UI에 데이터를 표시합니다.

DiffableDataSource를 CollectionView에 연결하려면, 해당 DataSource와 연결하려는 CollectionView를 전달하여 init(collectionView:CellProvider:) 이니셜라이저를 사용해 DiffableDataSource를 만듭니다.
또한, Cell Provider를 전달하여 UI에 데이터를 표시하는 방법을 결정하도록 각 셀을 구성합니다.

dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: UUID) -> UICollectionViewCell? in
    // Configure and return cell.
}

그런 다음 Snapshot을 구성하고 적용하여 데이터의 현재 상태를 생성하고, UI에 데이터를 표시합니다.
자세한 내용은 NSDiffableDataSourceSnapshot을 참고하세요.

.
.
.

네...! 정리하자면, CollectionView에 DataSource를 제공하기 위해서 DiffableDataSource를 만들어서 CollectionView에 연결해줘야하고, Cell Provider로 화면에 표시할 Cell을 구성한다. 이 과정을 생성자로 제공한다!

그리고, UI를 업데이트해주기 위해서 Snapshot을 생성하고, UI에 데이터를 표시한다!

또 하나 눈 여겨볼 것은 DiffableDataSource는 섹션(SectionIdentifierType)아이템(ItemIdentifierType)에 대한 제네릭 타입을 가집니다. 그리고, 두 타입 모두 Hashable 프로토콜을 채택해야합니다!

일단 여기까지 대충 이해했다고 치고, DiffableDataSource 생성자와 Snapshot에 대해서 좀 더 자세하게 알아볼게요.

생성자

지정된 Cell Provider를 사용하여 DiffableDataSource를 만들고, 지정된 CollectionView에 연결합니다.

Parameters

collectionView : 데이터 소스를 연결할 CollectionView
cellProvider : 데이터 소스가 제공하는 데이터를 사용해서 CollectionView의 각 Cell을 만들고 반환하는 클로저

NSDiffableDataSourceSnapshot

특정 시점의 View에 있는 데이터 상태의 표현

DiffableDataSource는 Snapshot을 사용하여 CollectionView 및 TableView에 데이터를 제공합니다. Snapshot을 사용하여 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영합니다.

그리고 DiffableDataSource와 마찬가지로 섹션과 아이템에 대한 제네릭 타입을 가지고, 역시 각각 Hashable 프로토콜을 준수해야만 합니다.

다음과 같이 Snapshot을 사용하여 뷰에 데이터를 표시합니다
1. Snapshot을 만들고 표시하려는 데이터의 상태로 스냅샷을 채웁니다.
2. Snapshot을 적용하여 UI에 변경 사항을 반영합니다.

// Create a snapshot.
var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()        


// Populate the snapshot.
snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID(), UUID()])


// Apply the snapshot.
dataSource.apply(snapshot, animatingDifferences: true)

이론은 여기까지 하고, 이제 예제 진행할게요!

예제 코드

전통적인 방식인 UICollectionViewDataSource와 DiffableDataSource의 가장 큰 차이점은 데이터가 변경될 때 reloadData 메서드를 호출 여부입니다. Snapshot의 개념이 추가되면서 현재 상태와 새로운 상태의 차이점을 비교하고, 이에 따라 필요한 UI 업데이트(셀 추가/삭제/재정렬 등)을 자동으로 수행합니다.
이러한 방식은 코드가 조금 더 복잡해질 수 있지만, 효율적인 UI 업데이트를 보장하며, 큰 데이터 세트에 대해서도 높은 성능을 유지할 수 있습니다.

enum DiffableSection: CaseIterable {
    case main
    // case etc... 섹션 추가
}

enum DiffableSectionItem: Hashable {
    case mainItem(MainItemModel)
    // case ectItem... 섹션 아이템 추가
    
    struct MainItemModel: Hashable {
        let title: String
    }
}
typealias MyDataSource = UICollectionViewDiffableDataSource<DiffableSection, DiffableSectionItem>
typealias MySnapshot = NSDiffableDataSourceSnapshot<DiffableSection, DiffableSectionItem>

Section과 Item을 열거형으로 정의하고, typealias로 DiffableDataSource와 Snapshot에 대한 타입을 사용자 정의 타입으로 지정했습니다.

굳이 typealias를 사용해서 타입을 따로 만들 필요는 없지만, 가독성을 위해서 좋은 방법입니다.

// Diffable DataSource 정의
    // 파라미터로 collectionView와 cellProvider 전달
    // cellProvider를 통해 UI에 표시할 셀을 리턴함
    private func setupDataSource() {
        dataSource = .init(collectionView: self.collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
            switch item {
            case .mainItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            }
        })
        
        dataSource?.supplementaryViewProvider = { (collectionView, elementKind, indexPath) -> UICollectionReusableView? in
            // header, footer...
            return nil
        }
    }
    
    // DiffableDataSource는 Snapshot을 사용해서 CollectionView 또는 TableView에 데이터를 제공
    // 스냅샷을 사용해서 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영함
    // 스냅샷의 데이터는 표시하려는 Section과 Item으로 구성됨.
    private func updateSnapshot(items: [DiffableSectionItem], toSection: DiffableSection) {
        var snapshot = MySnapshot() // 스냅샷 생성
        snapshot.appendSections(DiffableSection.allCases) // Section 추가
        snapshot.appendItems(items, toSection: toSection) // Item 추가
        dataSource?.apply(snapshot, animatingDifferences: true) // 데이터 새 상태 반영
    }

DiffableDataSource를 생성해서 collectionView에 연결합니다.
생성자 파라미터로 연결할 CollectoinView와 화면에 표시할 Cell을 구성하는 cellProvider를 클로저로 전달합니다.

또한, 데이터가 변경됐을 때, 변경사항을 업데이트하기 위해 snapshot을 생성해 섹션과 항목을 추가하고, 데이터 소스에 반영합니다.

    private func makeMockDatas() {
        var mockDatas = (1...100).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
        DispatchQueue.main.asyncAfter(deadline: .now()+1) {
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now()+3) {
            mockDatas = (20...120).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+5) {
            mockDatas = (40...120).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+7) {
            mockDatas = (30...45).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
    }

DiffableDataSource가 잘 동작하는지 테스트하기 위해 데이터를 생성하고, updateSnapshot 메서드를 호출합니다.

전체 소스코드


import UIKit

enum DiffableSection: CaseIterable {
    case main
    // case etc... 섹션 추가
}

enum DiffableSectionItem: Hashable {
    case mainItem(MainItemModel)
    // case ectItem... 섹션 아이템 추가
    
    struct MainItemModel: Hashable {
        let title: String
    }
}
typealias MyDataSource = UICollectionViewDiffableDataSource<DiffableSection, DiffableSectionItem>
typealias MySnapshot = NSDiffableDataSourceSnapshot<DiffableSection, DiffableSectionItem>


class DiffableDataSourceViewController: UIViewController {
    
    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = .init(width: view.frame.width - 20, height: 100)
        layout.scrollDirection = .vertical
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.register(RectangleCell.self, forCellWithReuseIdentifier: RectangleCell.identifier)
        return cv
    }()
    
    var dataSource: MyDataSource?
    
    override func viewDidLoad() {
        super.viewDidLoad()

        layout()
        setupDataSource()
        makeMockDatas()
    }
    
    private func layout() {
        view.backgroundColor = .white
        view.addSubview(collectionView)
        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
    
    private func makeMockDatas() {
        var mockDatas = (1...100).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
        DispatchQueue.main.asyncAfter(deadline: .now()+1) {
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now()+3) {
            mockDatas = (20...120).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+5) {
            mockDatas = (40...120).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+7) {
            mockDatas = (30...45).map { DiffableSectionItem.mainItem(.init(title: "main\($0)")) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
    }
}

//MARK: - DataSource
extension DiffableDataSourceViewController {
    
    // Diffable DataSource 정의
    // 파라미터로 collectionView와 cellProvider 전달
    // cellProvider를 통해 UI에 표시할 셀을 리턴함
    private func setupDataSource() {
        dataSource = .init(collectionView: self.collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
            switch item {
            case .mainItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            }
        })
        
        dataSource?.supplementaryViewProvider = { (collectionView, elementKind, indexPath) -> UICollectionReusableView? in
            // header, footer...
            return nil
        }
    }
    
    // DiffableDataSource는 Snapshot을 사용해서 CollectionView 또는 TableView에 데이터를 제공
    // 스냅샷을 사용해서 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영함
    // 스냅샷의 데이터는 표시하려는 Section과 Item으로 구성됨.
    private func updateSnapshot(items: [DiffableSectionItem], toSection: DiffableSection) {
        var snapshot = MySnapshot() // 스냅샷 생성
        snapshot.appendSections(DiffableSection.allCases) // Section 추가
        snapshot.appendItems(items, toSection: toSection) // Item 추가
        dataSource?.apply(snapshot, animatingDifferences: true) // 데이터 새 상태 반영
    }
    
}

CollectionView나 TableView를 구성할 때, 데이터 소스 업데이트가 자주 일어나고, 대량의 데이터를 다뤄야한다면, DiffableDataSource를 사용하는 것이 좋을 것 같습니다.
반면에 비교적 적은 양의 데이터와 단순한 목록 화면 같은 경우에는 전통적인 DataSource 방식으로 충분할 것 같습니다.

profile
iOS앱 개발자가 될테야

0개의 댓글