아요 개발 일기

[Swift] Diffable Datasource와 Compositional Layout 본문

Swift

[Swift] Diffable Datasource와 Compositional Layout

소진이 2023. 1. 29. 15:18

안녕하세요 :D

오늘은 Diffable Datasource와 Compositional Layout에대해 알아보도록 하겠습니다ㅎㅎ


Diffable Datasource? 🤔

WWDC19에서 Apple이 발표하였으며, iOS13 부터 사용이 가능합니다.

 

전에 저희가 Datasource를 사용했던 부분이 있었죠?

 

UITableViewDataSource

UICollectionViewDataSource

가 있었습니다.

 

그렇다면 Diffable Datasource는 어떤 것이 있을까요?

동일하게

 

UITableViewDiffableDataSource

UICollectionViewDiffableDataSource

 

가 있습니다.

 


 

Diffable Datasource가 왜 필요해?

 

우리가 CollectionView를 구성하게되면

UICollectionView에서 UICollectionViewDataSource를 conform하게됩니다.

 

 

즉, 위와 같은 코드로 작성이 되겠죠?

이렇게 UICollectionViewDataSource를 사용하면 간단하고 빠르게 작성이 가능하고 유연하다는 장점을 가지고 있습니다.

 

이러한 장점만 있다면 Diffable Datasource가 필요 없겠지만,

문제는 데이터가 변경될 때 발생합니다.

 

 

보통은 위 이미지와 같이 Controller가 데이터를 받아오고, UI에게 변경을 알립니다.

이 방식은 복잡한 구현이 생길수록 Controller와 UI가 서로 들고 있는 데이터가 일치하지 않게되는 이슈가 생기게 됩니다.

그렇게되면 앱에서는 어느것이 더 맞는 데이터인지 판단하기 어려워지게되고, 아래와 같은 에러가 뜨게되죠

 

 

해당 에러는 reloadData를 하게되면 해결은 됩니다.

하지만 애니메이션되지 않는 효과가 나타나 사용자 경험을 저하시킬 수 있게됩니다.

 

그렇다면 reloadData 말고 다른 방법은 없을까요?

 


 

Single Source Of Truth Data

 

위에서 설명한 것과 같이 기존의 구현 방식은

Controller와 UI가 각자 들고 있는 데이터 중에 어떠한 데이터가 참인지 알기 어렵다는 문제 점을 가지고 있었습니다.

 

그러면, 단순하게 참인 데이터를 하나만 두면 되겠죠?

이것을 Single Source of Truth 라고 하며, 

그렇게 제안된 방법이 Diffable Datasource 입니다.

 

 

Diffable DataSource에는 performBatchUpdates가 없기때문에

Crash나 번거로움, 복잡성, 처리하고 싶지 않은 모든 것들이 없으며

apply라는 단일 메소드만 사용합니다.

 


 

Snapshot

Single Source of Truth를 하기위해 도입된 것이 Snapshot 입니다.

 

Snapshot은 한가지 참인 데이터를 관리하는 객체로써,

Indexpath를 사용하지 않고, 섹션 및 아이템에 대해서 Unique ID를 사용합니다. 

- Unique + Hashable 

 

아이템 구현 예

 

함께 사진을 보면서 더 자세히 알아봅시다!

 

 

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

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

 

 

Apply만 하게 되면 아래 이미와 같이 새로운 Snapshot이 적용되게 됩니다.

 

 


 

Compositional Layout ?? 🤔

 

Compositional Layout도 WWDC19에서 Apple이 발표하였으며,

CollectionView Layout을 개선하기 위해 만들어졌습니다.

 

 

Compositional Layout이 왜 필요해?

 

 

기존에 UICollectionViewFlowLayout을 사용하면

대부분의 단순한 디자인에서 유용하며 Line-base 와 같은 장점이 있었습니다.

 

 

하지만,

Today's App Designs처럼 복잡한 디자인이 되었을때

CustomLayout을 그때마다 구현해주어야 하는 불편함이있었고

 

apple이 그 불편함을 해소하기위해 Composition Layout을 만들어주었습니다!

 

 

주요 Layout 개념

 

CollectionView를 구성하는 Layout은 크게 세가지 개념으로 구성을 해서 Layouting을 합니다.

 

 

이 세가지 개념을 이용하면 복잡한 구현도 쉽게 할 수 있습니다.

 

code 예제

 


사용해보기

 

 

 

 

 

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

 

DiffableDataSource는 Protocol이 아니라 Generic Class 입니다.

Generic은 타입에 제한을 두지 않는 특징을 가지고 있죠?

 

class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> 
: NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable

 

정의는 위와 같은데, Generic class이므로 적절한 타입을 넣어서 생성하면 됩니다.

여기서 

SectionIdentifierType과 ItemIdentifierType을 보면

Hashable 타입을 준수하고있죠?

이 부분을 기억해두고 넘어갑시다 ㅎㅎ

 

 

이제 직접 만들러 가봅시당

 

 

선언을 해주면 위와 같이 보여집니다.

먼저 Generic type을 지정해주려고하는데

아까 정의에서 봤던

SectionIdentifierType과 ItemIdentifierType을 넣어주면 되겠죠?

⭐️ 각 값들은 Hashable을 준수해야함! ⭐️

 

 

[SectionIdentifierType]

저는 Section을 enum으로 만들어주겠습니다

 enum Section {
        case main
 }

 

 


왜 enum을 사용해요? 🧐 

enum은 모든 case와 associated value가 Hashable을 준수하면 자동으로 synthesise 됩니다. 


 

[ItenIdentifierType]

저는 AppleFramwork라는 데이터를 만들어 놓았기때문에

 

var dataSource: UICollectionViewDiffableDataSource<Section, AppleFramwork>!

 

위처럼 item 값에는 AppleFramework를 넣어줘도 됩니다.

⭐️해당 AppleFramework 데이터는 Hashable 프로토콜을 준수하고 있어야 합니다.⭐️

 

하지만 저는 item 이름을 그대로 사용하고 싶어서

enum Section {
    case main
}

typealias Item = AppleFramework

var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

 

이렇게 선언해주도록할게요!

 

 

이제, collection View와 연결, Provider 구성을 해보겠습니다.

 

 

여기서 collectionView는 저희가 앞에 만들어 놓은 collectionView를 넣으면됩니다.

 

이제 cellProvider가 남았죠?

먼저 엔터를 눌러주면 코드가 아래와 같이 변경됩니다.

 

 

코드를 보면 cellProvider은 

collectionView, indexPath, itemIdentifier

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

 

우선 itenIdentifier을 저희가 만들어 놓은 item으로 변경해주도록 하겠습니다.

 

dataSource = UICollectionViewDiffableDataSource<Section,Item>(collectionView: collectionView, 
	cellProvider: { collectionView, indexPath, item in            
    // code
})

최종코드입니다!

 


 

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

이제 cell을 만들고 데이터를 넣어주는 작업을 해야겠죠?

dataSource = UICollectionViewDiffableDataSource<Section,Item>(collectionView: collectionView, 
	cellProvider: { collectionView, indexPath, item in            
    
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FrameworkCell", for: indexPath) as? FrameworkCell else {
        return nil
    }
    cell.configure(item)
    return cell
})

 


 

3. Generate the current state of the data.

snapshot을 사용하여 데이터를 다뤄보겠습니다

 

// data
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()

snapshot.appendSections([.main]) // Section에 어떤 item을 넣을지 지정
snapshot.appendItems(list, toSection: .main) // list를 .main Section에 넣어줌
dataSource.apply(snapshot) // dataSource에 적용

 

snapshot 생각보다 간단하죠?

 


 

4. Display the data in the UI.

데이터는 apply하면 UI가 적용이되지만

저희는 이번에 Composition Layout도 공부했기때문에 같이 적용해보겠습니다!

 

 

이 개념을 활용해서 코드를 짜봅시다!

 

[Item]

item은 Group 안에있으므로 Layout을 Group크기를 기준으로 설정합니다.

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

.factionWidth(0.33) : Group의 가로 길이를 3등분

.factionHeight(1) : Grop 높이와 같음

 

 

[Group]

Group은 Section 안에있으므로 Layout 기준은 Section입니다.

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.33))
let groupLayout = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3)

.factionWidth(1) : Section의 가로 길이와 같음

.factionWidth(0.33) : Section의 가로 길이를 3등분한 높이를 가짐

.horizontal(layoutSize: groupSize, subitem: item, count: 3)

 : layoutSize는 grouSize이며 subitem은 item 참조하며 3개씩 균일하게 보여줌

 

 

[Section]

let section = NSCollectionLayoutSection(group: groupLayout)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
section.interGroupSpacing = spacing

.contentInsets : section의 영역 설정

.interGroupSpacing : Group간 간격 설정

 

최종 코드

 private func layout() -> UICollectionViewCompositionalLayout {

        let spacing: CGFloat = 10
        
        // Item
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        // Group
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.33))
        let groupLayout = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3)
        
        // Section
        let section = NSCollectionLayoutSection(group: groupLayout)
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
        section.interGroupSpacing = spacing
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
}

 


 

정리

Diffable Datasource

1. 복잡한 구현이 생기면서 기존 방식인 UICollectionView을 사용하면 Controller와 UI가 각각 들고 있는 데이터가 일치하지 않는 문제 발생

2. 각각 데이터를 두지않고 참인 데이터 한개만 두는(Single Source of Truth) Diffable Datasource 방법이 제안됨

3. 그 참인 한개의 데이터를 관리하는 객체 Snapshot 도입

 

UICollectionViewCompositionalLayout

1. 기존에 사용하는 UICollectionViewFlowLayout은 단순한 디자인에서는 유용하게 사용됨

2. 하지만, 복잡한 디자인이 되었을때 CustomLayout을 구현해야하는 번거로움이있었음

3. 복잡한 디자인의 Layout을 더 쉽게 구현하기 위해 UICollectionViewCompositionalLayout 개발 됨.

 

 

참고 문헌