오랜만에 어떤 글을 쓸까 고민하다가 가장 많이 활용되는 것부터 하나씩 파헤쳐보자는 생각에 Diffable Datasource 관련된 글을 쓰기로 결정했습니다. 생각보다 양이 많을것 같아서 조금 주저했는데, 그래도 앞으로도 계속 사용될 친구이기 때문에 이 부분에 대해서 작성하기로 하였습니다. 먼저 Diffable의 데이터를 책임지는 SnapShot에 대해서 글을 쓰고 이후에 Layout과 dataSource에 대해서 글을 작성할 예정입니다. 문서를 기반으로 작성되었으며 하단에 레퍼런스 기재하였습니다.

 

NSDiffableDataSourceSnapshot

 

 

A representation of the state of the data in a view at a specific point in time. 특정 시점에 대해서 데이터에 대한 상태를 표현한것 이라고 되어있군요. iOS 13부터 사용이 가능하며, Generic Structure로써 SectionIdentifiertType 그리고 ItemIdentifierType을 받아야 하고 이는 모두 Hashable 과 Sendable 을 채택해야 합니다. 또한 앞에는 @proconcurrency 라는 propertyWrapper 가 달려있군요. 이 세가지에 대해서 한번 알아보겠습니다.

 

Hashable?

 

A type that can be hashed into a Hasher to produce an integer hash value. Hasher를 통해서 Int값인 HashValue를 제공할 수 있는 타입이라고 되어있습니다. 그리고 Equatable을 채택하는 것을 확인할 수 있습니다.

 

사실 여기서는 가볍게 넘어가지만 해쉬라는 개념은 컴퓨터 분야에서 굉장히 자주 사용됩니다. 간단하게 말씀드리자면 어떤 값에 대해 해쉬 함수를 통해서 해쉬 값을 얻습니다. 이를 통한 검색과 저장이 용이하며 O(1) 만큼의 시간만 걸리기 때문에 많이 사용됩니다. 물론 무조건 다 되는것도 아니고 상황에 따라서는 해쉬 충돌도 야기시킬 수 있습니다. 자세한 내용은 따로 공부하는 것을 추천드립니다.

 

Swift에서도 Hashable protocol은 범용적으로 활용이 되고 있습니다. String, Set, Dictionary 등등 값이 같은지 다른지 비교하는데 많이 사용됩니다. 따라서 어떤 상황에 따라서는 Hashable을 채택한 값을 받도록 하는 경우도 있으며, 직접 만든 타입에 Hashable 을 채택해야 하는 경우도 있습니다. 참고로 Swift 에서는 structure 의 모든 프로퍼티가 Hashable 을 채택하는 경우와 enum에서 모든 associated value가 Hashable 을 채택하는 경우 hash값을 자동으로 만들어줍니다. 여기서는 이정도만 알고 넘어가겠습니다.

 

Sendable?

 

A type whose values can safely be passed across concurrency domains by copying. concurrency 환경에서 복사를 통해 값이 안전하게 전달될 수 있는 타입을 말합니다. 기본적으로 Value Type, Reference Type중 mutable storage가 없거나 상태가 내부적으로만 관리되는 타입 그리고 function, closure 등이 있습니다. Sendable 의 경우 내부에 따로 채택해야할 함수나 값은 없지만, 상황에 따라서는 명시적으로 채택해야 한다고 합니다.

 

@Preconcurrency

 

결론만 얘기하자면 의도치 않은 warning을 제거하기 위함입니다. Swift 버전에 따라서 Sendable이 충분히 채택되지 않은 경우가 있는데 이 경우 warning이 발생할 수 있습니다. 이런 워닝을 없애기 위해서 만들었다고 하며, 정리하기에는 내용이 길기 때문에 하단에 레퍼런스 남겨두겠습니다. proposal link를 확인하시길 바랍니다.

 

NSDiffableDataSourceSnapshot Document

다시 돌아와서 문서쪽을 살펴보겠습니다. diffableDataSource는 Snapshot을 통해서 데이터를 관리합니다. 이를 통해서 디스플레이를 해주고 싶은 데이터를 관리하며 변경점 또한 반영할 수 있습니다. 이는 섹션과 아이템으로 만들어지며 추가 삭제 이동등의 작업을 할 수 있습니다.

 

스냅샷을 만들어서 이를 datasource에 적용 시킬때는 두가지 방식으로 접근할 수 있습니다. 빈 스냅샷을 만들어서 섹션과 아이템들을 넣는 방식과 기존 스냅샷을 가져와 변경하는 방식입니다. 아래 간단하게 코드로 예시를 나타냈습니다.

 

// 이렇게 선언해두면 사용하기에 편합니다.
private typealias SnapShot = NSDiffableDataSourceSnapshot<Int, String>
---
// 기본 데이터 - 1번째 방식
var snapShot = SnapShot()
snapShot.appendSections([0, 1])
snapShot.appendItems(["hello", "world"], toSection: 0)
snapShot.appendItems(["hello2", "world2"], toSection: 1)
dataSource.apply(snapShot) 
---- 
// 데이터 추가 - 2번째 방식
var originSnapshot = dataSource.snapshot()
originSnapshot.deleteItems(["world2"]) 
// or
originSnapshot.appendItems(["world3"], toSection: 1)
dataSource.apply(originSnapshot, animatingDifferences: true , completion: nil)

 

일단 dataSource.apply는 다음에 알아볼 예정이므로 저렇게 하면 적용이 되겠구나 까지만 알아두시면 되겠습니다. 이렇게 기존에 있던 dataSource에 접근하여 snapshot을 가져오면 현재 data가 담겨져 있는 snapshot을 가져올 수 있고 이를 수정하여 적용시킬 수 있습니다.

 

중요한점은 각각의 섹션과 아이템은 Hashable을 채택한 unique identifiers를 가져야 합니다. 만약 class를 identifier로 사용한다면 해당 클래스는 NSObject를 상속받아야 합니다. 참고로 itemidentifier는 Hash Value입니다. 그래서 itemIdentifierType은 Hashable을 채택해야 사용할 수 있는 것입니다. 이는 문서에 명시적으로는 나와있지 않으나 WWDC 2019 에서 확인할 수 있었습니다.

 

For our mountain type, we'll look at our Mountains Controller, which is again, our model layer. And here, we see that we've declared mountain as a Swift struct. And we declared that struct type as hashable so that we can use it with DiffableDataSource natively rather than explicitly have to pass an identifier. And the important requirement there is just that each mountain be uniquely identifiable by its hash value. So we achieve this by giving each mountain an automatically generated unique identifier.

WWDC2019 : Advances in UI Data Sources

 

Q : 동일한 데이터를 계속 넣고 싶다면?

 

우리는 데이터를 item으로 변환하여 snapshot에 적용하고 있습니다. 위에서 설명을 보셨다시피 동일한 데이터를 통해서 동일한 item을 넣는것은 안된다고 하였습니다. 왜냐면 어떤 아이템에 대해서 어느 섹션에 어디에 위치시킬지 명확하게 알기 위해서입니다. 그리고 이것이 중복이 된다면 더 이상 명확하지 않기 때문입니다. 마치 기존에 indexPath는 어떠한 셀이라도 다른것과 마찬가지 입니다. 그렇다면 다른 섹션에는 동일한 데이터라도 괜찮지 않을까요?? 그렇지 않습니다. 이는 snapshot이 데이터를 어떻게 다루는지를 보면 알수 있는데, 예를 들어서 deleteItems([ItemIdentifierType]) 을 봤을때. 어떤 섹션에 있는 아이템들을 삭제하는지 명시하지 않습니다. 즉 스냅샷에 대해서 아이템들은 유일한 identifier를 가진다는 것을 의미합니다. 따라서 스냅샷은 섹션과 아이템 모두 유일하여야 합니다.

 

하지만 현실에서는 상황에 따라 동일한 UI를 계속해서 보여줘야 하는 경우가 있을수도 있습니다. 그렇다면 어떻게 해야 할까요? 만약 1이라는 숫자를 3개의 셀에서 동시에 보여줘야 한다면??? 같은 테이블뷰 혹은 같은 섹션이라면??? 보여지는 것은 1 이더라도 각 셀은 유일한 identifier를 가져야 합니다. Int만으로는 동일한 1이라는 숫자를 표현할 수 없습니다. 1의 해쉬값은 동일하니까요. 따라서 새로운 타입이 추가되어야 하며 이는 Hashable 을 채택해야 합니다. 다음과 같이 선언하고 value 값만 가져와서 사용하면 됩니다.

 

struct NewItem: Hashable {
    private let id = UUID()
    var value: Int
}

 

이렇게 일단 Snapshot에 대해서 간단하게 알아보았습니다.

 

Reference

https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot

https://developer.apple.com/documentation/swift/hashable/

https://developer.apple.com/documentation/swift/sendable

https://github.com/apple/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md

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

+ Recent posts