[iOS] 북킵: Realm 변경사항을 DiffableDataSource에 반영하고 뷰에 바인딩하기

0
post-thumbnail

Overview

북킵의 홈 화면은 CollectionView로 크게 2색션으로 나뉩니다. 상단에는 셀1개씩 가로 스크롤로 보여주는 "읽고있는 책", 그리고 하단에는 2개의 셀을 수직 스크롤로 보여주는 "읽을 예정인 책"이 있습니다.

DiffableDataSource를 사용할 경우의 장점은 기본적인 animation이 지원된다는 점과index가 아닌 데이터기반으로 셀들을 생성하기 때문에 index로부터 자유롭게 각 섹션, 그룹, 아이템을 변경 할 수 있다는 점입니다.

ViewModel은 Realm을 구독하고 있고 Realm의 변경이 감지되면 ViewModel이 들고있는 데이터 인스탄스의 변경과 함께 뷰의 업데이트가 일어나도록 구현되어있습니다.

Troubleshooting

Realm의 공식문서에서 제공하는 코드를 참고하여 Realm Results를 구독하였고 성공적으로 상태값을 실시간으로 받아오는것을 확인했습니다.

//listen for changes of realm objects and update the observables
    private func observeRealmChanges(for observable: Observable<Results<RealmBook>>){
        let token = observable.value.observe { changes in
            switch changes {
            case .initial(let results):
                observable.value = results
            case .update(let results, deletions: _, insertions: _, modifications: _):
                observable.value = results
            case .error(let error):
                dump(error)
                dump(RealmError.nonExist)
            }
        }
        notificationTokens.append(token)
    }

따라서 모델안에 있는 readingStatus enum 의 따라 Diffable Data Source의 기본 에니메이션과 함께 섹션이 바뀌었으나 사용하는 CollectionViewCell이 재적용되지 않는 문제가 발생했습니다.

Solution

lldb po를 쓰면서 값을 트래킹하고 breakpoint를 걸면서 코드를 추적한 결과 Layout Enum에 따라 등록된 셀을 반환해주는 switch문이 변경시엔 실행되지 않고있음을 발견했습니다.

dataSource = UICollectionViewDiffableDataSource<SectionLayoutKind, RealmBook>(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
let status = itemIdentifier.readingStatus
switch status {
	case .reading:
	    return collectionView.dequeueConfiguredReusableCell(using: readingCellRegistration, for: indexPath, item: itemIdentifier)
    case .toRead:
    	return collectionView.dequeueConfiguredReusableCell(using: toReadCellRegistration, for: indexPath, item: itemIdentifier)
    default:
	    return collectionView.dequeueConfiguredReusableCell(using: toReadCellRegistration, for: indexPath, item: itemIdentifier)
    }

따라서 처음에는 bind()의 클로저 내에서 snapshot을 적용이 잘못된걸로 생각이 되었고 dataSource.applySnapshotUsingReloadData() 메소드를 사용해서 해결했으나 animation이 적용되지 않았고 그렇다면 기존 UICollectionView.reloadData()를 쓰는것과 차이가 없게 됩니다.

애플이 제공하는 자료에 따라 Snapshot들은 서로의 차이를 가지고 변경된다는 점을 깊게 고민했습니다.

현재 제 코드는 readingStatus의 따라 각 아이템이 섹션 위 아래로 자유자재로 이동해야 하는데 이 과정에서 snapshot.deleteItems()와 snapshot.appendItems()가 실행되고 dataSource.apply(snapshot,...)을 하는 방식이었고 코드는 다음과 같았습니다.

func moveSection(itemToMove: RealmBook,from sourceSection: SectionLayoutKind, to destinationSection: SectionLayoutKind) {
	snapshot.deleteItems([itemToMove])        
	snapshot.appendItems([itemToMove], toSection: destinationSection)
    dataSource.apply(snapshot, animatingDifferences: true)
}

해당 부분을 아이템을 삭제했을 때 apply()를 호출하고 아이템이 추가 되었을때 다시한번 apply()를 호출함으로써 명확하게 snapshot의 변경이 일어났음을 인지되어 cellProvider가 재호출됨을 확인하였습니다.

func moveSection(itemToMove: RealmBook,from sourceSection: SectionLayoutKind, to destinationSection: SectionLayoutKind) {
	snapshot.deleteItems([itemToMove])
    dataSource.apply(snapshot, animatingDifferences: true)

	snapshot.appendItems([itemToMove], toSection: destinationSection)
    dataSource.apply(snapshot, animatingDifferences: true)
}

결론

아마 Diffable Data Source를 사용하지 않았다면 이 화면은 훨씬 더 빨리 구현할 수 있었을것입니다. 그러나 이번 기회로 애플이 추구하는 Data기반 UI를 몸소 체험할 수 있었고 앱에 기능이 더해져 다양한 곳에서 UseCase가 발생하여도 Single Source of Truth를 통해 뷰의 일관성을 보장할 수 있게 되었습니다.

0개의 댓글