안녕하세요! 오늘은 Diffable DataSource를 활용하여 value type 아이템들의 데이터 변경을 어떻게 하는지에 대해 포스팅을 써보려 합니다.
위의 동영상처럼 상세화면에서 하트 인터렉션이 발생했을 때, 리스트 화면에서도 하트 정보가 업데이트 되어야하는 상황으로 예시를 들어볼게요!
무슨 말이냐 하면, 참조 타입의 경우엔 스냅샷의 reloadItems(_:)
를 활용하여 지정된 항목을 다시 로드할 수 있지만, 값 유형의 경우엔 reloadItems(_:)
는 작동하지 않으므로 스냅샷 내에서 업데이트 된 항목을 수동으로 교체해야 한다는 것입니다.
바로 코드로 볼게요!
ItemIdentifier
타입에 객체를 넣어줍니다.var dataSource: UICollectionViewDiffableDataSource<CommunityCategoryType, StoryNew>!
// 1. IndexPath로 해당 item 찾기
guard let communityStories = dataSource.itemIdentifier(for: indexPath) else { return }
// 2. 기존 객체를 새로운 객체로 업데이트
communityStories = newStory
// 3. 수정을 위한 새로운 snapshot 생성
var newSnapshot = dataSource.snapshot()
// 4. reload
newSnapshot.reloadItems([communityStories])
// 5. apply changes
dataSource.apply(newSnapshot)
처음에 언급했듯이, 값 타입의 아이템은 reloadItems(_:)
에서 작동하지 않습니다.
저도 프로젝트 진행하면서 위의 방법으로는 업데이트가 안되는 거에요.. 왜일까 생각해보니 대부분의 모델이 값 타입인 Struct로 구현이 되어있는데 참조 타입 방식으로 업데이트 하려하니 안되는 거였어요!
간단히 말하면, StoryNew가 value type이라면, communityStories는 StoryNew의 새 인스턴스가 될거고, 스냅샷 내에서 communityStories로 객체를 가리키지 않을거죠? 따라서 newSnapshot에서 communityStories를 다시 로드하려고 하면
“Invalid item identifier specified for reload“
라는 이유로NSInternalInconsistencyException
예외가 발생할거에요.
그래서 찾은 해경방법은! 스냅샷 내에서 기존의 객체를 새 객체로 교체하면 가능합니다!
ItemIdentifier
타입에 String
을 넣어줍니다.var dataSource: UICollectionViewDiffableDataSource<CommunityCategoryType, StoryNew>!
// 1. IndexPath로 해당 item 찾기
guard let communityStories = dataSource.itemIdentifier(for: indexPath) else { return }
// 2. 새로운 스토리 카피를 생성 & 업데이트
var updatedStories = communityStories
updateStories = newStory
// updateStories.heartButton.isSelected = true 이런식으로 바꿔도 상관없음.
// 3. 수정을 위한 새로운 snapshot 생성
var newSnapshot = dataSource.snapshot()
// 4. 업데이트 된 객체로 replace
newSnapshot.insertItems([updateStories], beforeItem: communityStories)
newSnapshot.deleteItems([communityStories])
newSnapshot.reloadItems([communityStories])
// 5. apply changes
dataSource.apply(newSnapshot)
근데 보면 음.. insert / delete로 업데이트를 해주다 보니 뒤로 가면 스크롤 위치가 계속 바뀌더라구요!
이러한 불편함을 없애기 위해 찾아낸 방법도 공유해볼게요!
고유한 식별자를 추가
하는 것입니다. 이 식별자를 저는 identifier
로 지정하고 UUID()
를 이 식별자로 사용할게요!struct StoryNew: Codable, Hashable {
var identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: StoryNew, rhs: StoryNew) -> Bool {
return lhs.identifier == rhs.identifier
}
}
UUID
으로 바꿔줍니다.var dataSource: UICollectionViewDiffableDataSource<Section, UUID>!
var storyDic = [UUID: StoryNew]()
[StoryNew]
를 [(UUID, StoryNew)]
로 변환한 후, Dictionary(uniqueKeysWithValues:)
를 사용해서 [UUID : StoryNew]
로 변환해볼게요.// convert `[StoryNew]` -> `[(UUID, StoryNew)]`
let tupleArray = stories.map { ($0.identifier, $0) }
// convert `[(UUID, StoryNew)]` -> `[UUID : StoryNew]`
self.storyDic = Dictionary(uniqueKeysWithValues: tupleArray)
모델 객체
가 아닌 UUID
를 제공하기 때문에 cell registration handler도 살짝 변경이 필요하겠죠!let cellRegistration = UICollectionView.CellRegistration<CommunityCollectionViewCell, UUID> { (cell, indexPath, uuid) in
// `uuid`를 사용하여 Story 가져오기
if let story = self.viewModel.storyDic[uuid] {
cell.setData(story: story)
}
}
Cell Registration 핸들러 내에서 주목해야 할 점은 storyDic을 사용하여 해당 스토리 객체를 가져오는 방법입니다.
// <Section, UUID>
var dataSource: UICollectionViewDiffableDataSource<CommunityCategoryType, UUID>!
// 스냅샷 items로 모든 `identifier` 추가
snapshot.appendItems(stories.map { $0.identifier }, toSection: categoryType)
dataSource.apply(snapshot, animatingDifferences: true)
이렇게 하면 모델 객체의 식별자를 data source item 식별자로 사용하도록 성공적으로 변환 성공!
기존과의 차이점은 data source가 모델 객체 대신 identifier를 제공한다는 것입니다. 따라서 우리는 storyDic을 사용하여 story를 가져와 그에 따라 업데이트를 해야합니다.
func updateItem(selectedId: UUID) {
var updated = storyDic[selectedId]!
// story 하트 업데이트하고 그에 따라 `storyDic` 업데이트
if let isLiked {
updated.isMyEmpathized = isLiked
updated.nEmpathies = likeCount ?? (updated.nEmpathies ?? 0) + (isLiked ? 1 : -1)
storyDic[selectedId] = updated
}
// 수정을 위해 데이터 소스 스냅샷의 새 복사본 생성
var newSnapshot = dataSource.snapshot()
if #available(iOS 15, *) {
// iOS 15
// `newSnapshot`에서 업데이트해야 하는 항목의 데이터 지정하기 (`StoryNew.identifier`를 사용)
newSnapshot.reconfigureItems([selectedId])
} else {
// iOS 14
newSnapshot.reloadItems([selectedId])
}
// data source에 `newSnapshot` 적용하여 변경 사항이 컬렉션 뷰에 반영되도록 함
dataSource.apply(newSnapshot)
}
이렇게 data source item identifier에 모델 객체 대신 고유 식별자 (identifier)를 저장하니 reference / value 타입에서 전부 다 작동되는 것을 확인했습니다!
스크롤 위치가 변경되지 않고 그대로 유지되고 셀 업데이트도 정상적으로 작동 되는게 보이죠~?
여담으로 iOS 15 이상부터 사용 가능한
reconfigureItems(_:)
가 왜reloadItems(_:)
보다 훨씬 성능이 좋을까요??
그 이유는 reconfigureItems"
는 변경된 항목만 업데이트하고 새로 그리는 대신, 필요한 변경 사항에만 중점을 둡니다. 반면 reloadItems
는 셀을 완전히 다시 그리기 때문에 더 많은 작업을 수행해야 합니다.
결국 reconfigureItems
는 더 작은 범위의 변경 사항을 처리하고 셀 렌더링을 최적화하는 데 중점을 두어 더 빠른 결과를 제공하는 거네요! (iOS 15 이상부터는 reconfigreItems를 사용하자~~😃)
아래 WWDC 영상에서 Diffable DataSource 관련한 더 많은 개선점들이 나와있으니 보시는 걸 추천드릴게요!
https://developer.apple.com/videos/play/wwdc2021/10252/
https://developer.apple.com/documentation/uikit/uiimage/building_high-performance_lists_and_collection_views (Sample Code)