개발을 잘하고 싶은 주니어?

Diffable Data Source 공부하기 - 2 (실습) 본문

개발/iOS

Diffable Data Source 공부하기 - 2 (실습)

데쿠! 2021. 11. 21. 02:58
반응형

앞서 Diffable Data Source 공부하기 - 1 (이론)에서 알 수 있듯이 diffable은 현재 화면에 보이는 아이템을 업데이트할 때마다 collection view가 자동으로 업데이트된 collection과 이전의 것의 차이를 계산해줍니다.

또한 data source의 값과 collection view가 알고있는 값의 동기화도 해주고 그런 데이터 변화의 관리도 맡아서 합니다.

 

예시 동영상

 

Section, Item 클래스 생성

UICollectionViewDiffableDataSource는 두개의 generic 타입을 가집니다. (Section 타입과 item 타입)

section type은 다음과 같이 class 타입으로 만들어 줍니다. 만약 고정된 개수와 고정된 섹션을 가진다면 enum 타입으로 생성해도 됩니다. 하지만 여기서는 class 타입으로 해보겠습니다.

// 1
class Section: Hashable{
    let id = UUID()
    var title: String
    var people: [Person]
    init(title: String, people: [Person]){
        self.title = title
        self.people = people
    }
    // 2
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    // 3
    static func == (lhs: Section, rhs: Section) -> Bool {
        lhs.id == rhs.id
    }
}

// 4
extension Section{
    static var sections: [Section] = [
        Section(title: "intj", people: [
            Person(name: "Kang Hee Seon", age: 26),
            Person(name: "Kim Eun Hae", age: 21)
        ]),
        Section(title: "estp", people: [
            Person(name: "Choi Hong Gyu", age: 32)
        ]),
        Section(title: "istp", people: [
            Person(name: "Choi Soo", age: 16),
            Person(name: "Kim Jung Wook", age: 27),
            Person(name: "Kim Dae Hyuck", age: 12)
        ]),
        Section(title: "entj", people: [
            Person(name: "Baek Eun Soo", age: 42)
        ])
    ]
}

차례대로 보겠습니다.

1. 우선 diffable datasource의 generic타입들은 모두 hashable 해야 합니다. 왜냐하면 데이터가 추가되거나 업데이트되거나 삭제되거나 하는 작업이 진행될 때 차이를 계산하는 과정에서 두 개의 요소가 같은지 다른지를 알아봐야 하기 때문입니다.

2, 3. Hashable은 Equatable 프로토콜을 채택하고 있기 때문에 ==과 hash 메서드 둘 다 구현해주어야 합니다.

만약 struct나 enum같은 값 타입이라면 컴파일러가 자동으로 Hashable프로토콜을 채택하게 하지만 class와 같은 레퍼런스 타입 같은 경우에는 일일이 필요한 메서드를 추가해주어야 합니다.

hash함수 자체를 구현하는 것은 복잡하기 때문에 따로 구현을 하지 않습니다. 제공된 hasher를 이용해서 구분을 위해 필요한 값을 combine메서드의 인자 값으로 넘겨줍니다. 여기서는 id값이 됩니다.

4. 화면에 보여줄 섹션들을 미리 만들어 놓았습니다.

 

Person 클래스도 위와 같이 동일하게 Hashable프로토콜을 채택해서 원하는 값을 추가해줍니다.

import UIKit
class Person: Hashable{
    let id = UUID()
    var name: String
    var age: Int
    init(name: String, age: Int){
        self.name = name
        self.age = age
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.id == rhs.id
    }
}

 

Header View 생성

import UIKit
// 1
class SectionHeaderReusableView: UICollectionReusableView{
    static var identifier: String {
        return String(describing: SectionHeaderReusableView.self)
    }
    // 2
    lazy var titleLabel: UILabel = {
        var label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title1).pointSize, weight: .bold)
        label.textColor = .label
        label.textAlignment = .left
        label.numberOfLines = 1
        label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 3
        backgroundColor = .systemBackground
        addSubview(titleLabel)
        
        NSLayoutConstraint.activate([
          titleLabel.leadingAnchor.constraint(
            equalTo: readableContentGuide.leadingAnchor),
          titleLabel.trailingAnchor.constraint(
            lessThanOrEqualTo: readableContentGuide.trailingAnchor)
        ])
        
        NSLayoutConstraint.activate([
          titleLabel.topAnchor.constraint(
            equalTo: topAnchor,
            constant: 10),
          titleLabel.bottomAnchor.constraint(
            equalTo: bottomAnchor,
            constant: -10)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

1. Header View의 경우에는 UICollectionViewReusableView 를 상속받습니다. 헤더 뷰 또한 셀처럼 재사용이 되기 때문입니다.

2. 섹션 타이틀에 대한 설정을 해줍니다. 

3. 현재 뷰에 위에서 생성한 레이블을 추가해주고 오토레이아웃을 설정합니다.

 

Daffable Data Source 생성

private var sections = Section.sections

typealias DataSource = UICollectionViewDiffableDataSource<Section, Person>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Person>

private lazy var dataSource = makeDataSource()

DiffableDataSource<>와 Snapshot<>에 대한 선언이 길어서 typealias를 이용해서 짧게 줄여줍니다.

DataSource는 제네릭 타입을 가진 클래스이고, 두 개의 타입은 Hashable해야 합니다.

open class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject, UICollectionViewDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {

Snapshot은 NSDiffableDataSourceSnapshot은 현재 section과 item에 대한 정보를 가지고 있습니다. 그래서 diffable datasource가 이것을 보고 섹션과 아이템이 얼마나 있는지를 파악할 수 있습니다.

public struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {

또한 위와 같이 dataSource를 설정할 때에는 꼭 lazy로 해야 합니다. 그 이유는 실제로 makeDataSource()가 실행이 되어서 dataSource를 리턴하기도 전에 View Contorller는 dataSource가 초기화되기를 원하면서 오류를 보여주기 때문입니다.

lazy를 사용하면 실제 이 변수가 사용될 때 초기화가 되므로 오류가 사라집니다.

lazy를 하지 않았을 때 발생하는 오류

 

그럼 이제 makeDataSource함수를 보겠습니다.

func makeDataSource() -> DataSource{
    // 1
    let dataSource = DataSource(collectionView: collectionView) { (collectionView, indexPath, person) -> UICollectionViewCell? in
        // 2
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCollectionViewCell", for: indexPath) as? PersonCollectionViewCell
        cell?.person = person
        return cell
    }
    // 3
    dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
        // 4
        guard kind == UICollectionView.elementKindSectionHeader else { return nil }
        // 5
        let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeaderReusableView.identifier, for: indexPath) as? SectionHeaderReusableView
        // 6
        let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
        view?.titleLabel.text = section.title
        return view
    }
    return dataSource
}

1. datasource를 생성할 때에는 collectionview와 cell provider를 파라미터로 넘겨줘야 합니다.

2. 또한 cell provider에서는 실제 cell을 리턴하는 클로저를 파라미터로 넘겨줍니다. 여기서 생성한 셀은 우리가 화면에서 보는 셀을 의미하고, collectionView(_:cellForItemAt:)에서 작성한 코드랑 동일하게 작성하면 됩니다.

3. 우리는 헤더뷰도 추가하기를 원하기 때문에 dataSource.supplementaryViewProvider에 헤더 뷰를 생성하는 클로저를 할당해줍니다.

4. 헤더뷰를 원하기 때문에 만약 kind가 UICollectionView.elementKindSectionHeader인지를 체크합니다.

5. 그리고 나서, 앞에서 만들었던 SectionHeaderReusableView 클래스를 가지고 SupplementaryView를 재사용하는 코드를 추가합니다.

6. 그리고 나서, 헤더 뷰의 타이틀을 현재 section의 title로 설정해주면 됩니다. 

// DiffableDataSource
nonisolated open func snapshot() -> NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>

// Snapshot
public var sectionIdentifiers: [SectionIdentifierType] { get }

dataSource의 snapshot()은 현재 참조하고 있는 snapshot을 말하며, 이 snapshot의 sectionIdentifiers는 현재 snapshot이 저장하고 있는 섹션들을 저장하고 있는 배열입니다. indexPath.section을 통해서 section 정보를 가져오고, 타이틀을 설정해줍니다.

 

cellProvider: diffable datasource에서 collectionview의 cell을 생성하고 리턴하는 클로저를 말합니다. 

supplementaryViewProvider: diffable datasource에서 collectionview의 supplementaryViewProvider(header or footer)를 생성하고 리턴하는 클로저를 말합니다. 

실제로 우리는 diffable datasource를 사용하기 때문에 기존에 datasource 프로토콜을 채택해서 사용했던 메서드들을 클로저를 사용해서 구현을 합니다.

 

 

Snapshot apply

// 1
func applySnapshot(animatingDifferences: Bool = true){
    var snapshot = Snapshot()
    // 2
    snapshot.appendSections(sections)
    sections.forEach { section in
        snapshot.appendItems(section.people, toSection: section)
    }
    // 3
    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

1. dataSource가 snapshot을 참조하는 함수입니다. animatingDifferences는 UI를 업데이트할 때 애니메이션을 포함할 것이냐에 대한 것입니다.

2. 현재의 sections를 snapshot의 sections로 추가해주고 appendItems를 통해 각 섹션에 대한 아이템들도 추가해줍니다.

3. dataSource의 apply메서드를 사용해서 현재 snapshot의 정보를 가지고 UI를 업데이트해줍니다. 

diffable data source는 collection view (UI)의 현재 상태와 snapshot (데이터)의 차이를  새로운 상태를 O(n)으로 계산합니다. 여기서 n은 snapshot의 아이템 개수입니다.

 

 

위의 헤더뷰 생성과 뷰 컨트롤러의 코드는 raywenderich의 코드를 참고하였습니다. https://www.raywenderlich.com/8241072-ios-tutorial-collection-view-and-diffable-data-source

 

iOS Tutorial: Collection View and Diffable Data Source

In this iOS tutorial, you’ll learn how to implement a collection view with UICollectionViewDiffableDataSource and NSDiffableDataSourceSnapshot.

www.raywenderlich.com

 

 

반응형

'개발 > iOS' 카테고리의 다른 글

Hex to Color  (0) 2021.11.23
URLSession 공부하기 - 3  (0) 2021.11.23
URLSession 공부하기 - 2 (실습)  (0) 2021.11.20
URLSession 공부하기 - 1 (이론)  (0) 2021.11.19
Diffable Data Source 공부하기 - 1 (이론)  (0) 2021.11.05
Comments