WooKoo Blog

물과 같이

개발/개발

[iOS] - SkeletonView 를 RxDataSources 에 적용해보기

WooKoo 2022. 10. 31. 21:13

안녕하세요 !

 

회사에서 컬렉션뷰 혹은 테이블뷰에 스켈레톤뷰를 적용해야해서 어떻게 할까 인터넷을 검색해보았습니다.
https://github.com/Juanpe/SkeletonView

 

GitHub - Juanpe/SkeletonView: ☠️ An elegant way to show users that something is happening and also prepare them to which con

☠️ An elegant way to show users that something is happening and also prepare them to which contents they are awaiting - GitHub - Juanpe/SkeletonView: ☠️ An elegant way to show users that something ...

github.com

많은 분들이 이 라이브러리 를 사용하시더라구요


근데 저희 앱에 구현되어있는 테이블뷰는 이런식이었습니다.

ViewModel.data
 .bind(to: collectionView.rx.items) { collectionView, row, element in
 let cell = UICollectionViewCell()
 return cell
 }
 .disposed(by: disposeBag)

대강 요런 식으로 그냥 모델에 바인딩해주는 방식이었어요.

 

근데 저기 SkeletonView 리드미에 보면 일반  UIKit 을 사용 할 때 쓰는 Delegate 방식에서의 방법만 제공해주고 있어서 많이 알아봤습니다.

 

음... 많이 찾아보니 방법이 없어보이더라구요.. 그렇다고 스켈레톤뷰를 직업 구현하기도 까다롭고...

 

찾다보니 https://github.com/AnyOptional/RxSkeleton

 

GitHub - anyoptional/RxSkeleton: Support for using SkeletonView with RxDataSources.

Support for using SkeletonView with RxDataSources. - GitHub - anyoptional/RxSkeleton: Support for using SkeletonView with RxDataSources.

github.com

오래된 라이브러리이지만 어떤분이 이전에 만들어두신게 있더라구요.

 

그럼 스켈레톤뷰를 적용하기위해서 먼저 RxDataSources 로 변경해야합니다.

(어쩔 수 없잖아... 다시 리팩토링해..!!)

 

이 글에서 RxDataSources 를 이용한 컬렉션뷰나 테이블뷰 만드는 방법은 다루지 않겠습니다. 

 

자 이제 리팩토링을 다 했다고 가정하고

 

위 라이브러리에
https://github.com/anyoptional/RxSkeleton/tree/master/Sources

 

GitHub - anyoptional/RxSkeleton: Support for using SkeletonView with RxDataSources.

Support for using SkeletonView with RxDataSources. - GitHub - anyoptional/RxSkeleton: Support for using SkeletonView with RxDataSources.

github.com

 

Sources 폴더에 있는 모든 코드를 가져옵니다. Rx 를 확장해서 사용하는 델리게이트 프록시 느낌이에요

 

오래 된 코드라 조금 손봐줘야하는데 빌드해보시고 E 라고 되어있는 부분에서 오류 날텐데 Element 로 수정하시고!

 

RxDataSources 구현 하실 때

RxTableViewSectionedReloadDataSource<Section>

 

이렇게 configureCell 해주셨던 부분 기억하시나요?

 

여기를 

 

RxCollectionViewSkeletonedReloadDataSource<Section>

 

이렇게 확장된 스켈레톤 RxDataSource 를 이용하시면 됩니다!!

 

어라..? 근데 어떤 식으로 확장한건지 궁금하지 않나요??

 

간단하게 둘러보겠습니다.

 

저희가 사용한 RxCollectionViewSkeletondReloadDataSources 라는 폴더를 볼게요 

//
//  RxCollectionViewSkeletonedReloadDataSource.swift
//  RxSkeleton
//
//  Created by Archer on 2018/11/30.
//

import SkeletonView
import RxDataSources

public class RxCollectionViewSkeletonedReloadDataSource<S: SectionModelType>: RxCollectionViewSectionedReloadDataSource<S>,
                                                                              SkeletonCollectionViewDataSource {

    public typealias NumberOfSections = (RxCollectionViewSkeletonedReloadDataSource<S>, UICollectionView) -> Int
    public typealias NumberOfItemsInSection = (RxCollectionViewSkeletonedReloadDataSource<S>, UICollectionView, Int) -> Int
    public typealias ReuseIdentifierForItemAtIndexPath = (RxCollectionViewSkeletonedReloadDataSource<S>, UICollectionView, IndexPath) -> String
    
    var numberOfSections: NumberOfSections
    var numberOfItemsInSection: NumberOfItemsInSection
    var reuseIdentifierForItemAtIndexPath: ReuseIdentifierForItemAtIndexPath
    
    public init(configureCell: @escaping ConfigureCell,
                configureSupplementaryView: ConfigureSupplementaryView? = nil,
                moveItem: @escaping MoveItem = { _, _, _ in () },
                canMoveItemAtIndexPath: @escaping CanMoveItemAtIndexPath = { _, _ in false },
                numberOfSections: @escaping NumberOfSections = { _, _ in 1 },
                numberOfItemsInSection: @escaping NumberOfItemsInSection = { _, cv, _ in
        guard let flowlayout = cv.collectionViewLayout as? UICollectionViewFlowLayout else { return 0 }
        return Int(ceil(cv.frame.height/flowlayout.itemSize.height))
        },
                reuseIdentifierForItemAtIndexPath: @escaping ReuseIdentifierForItemAtIndexPath) {
        self.numberOfSections = numberOfSections
        self.numberOfItemsInSection = numberOfItemsInSection
        self.reuseIdentifierForItemAtIndexPath = reuseIdentifierForItemAtIndexPath
        super.init(configureCell: configureCell,
                   configureSupplementaryView: configureSupplementaryView,
                   moveItem: moveItem, canMoveItemAtIndexPath: canMoveItemAtIndexPath)
    }
    
    public func numSections(in collectionSkeletonView: UICollectionView) -> Int {
        return numberOfSections(self, collectionSkeletonView)
    }
    
    public func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return numberOfItemsInSection(self, skeletonView, section)
    }
    
    public func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> String {
        return reuseIdentifierForItemAtIndexPath(self, skeletonView, indexPath)
    }
}

 

원래 사용하던 RxCollectionViewSectionedReloadDataSource 를 채택하고 있고 구현 시 필요한 데이터를 추가적으로 반환해주있어요

 

또한 UICollectionView+Rx 파일을 보면 

Items 를 RxCollectionViewSkeletonedDataSourceProxy 를 통해서 사용하는 것을 볼 수 있어요.

 

델리게이트 프록시에서는 RxDataSources 로 구현한 내용을 토대로 다시 UIKit에서 구현하듯이 다시 적용하는 모습을 볼 수 있는 것 같아요.

 

Delegate Proxy 에 대한 이해가 많이 부족해서 다음에는 Delegate Proxy가 어떻게 동작하는지 원리를 파악해봐야겠습니다!

 

아무튼 기존 방법대로는 사용 할 수 없지만 RxDataSources 롤 사용 할 수 있어 무리없이 해결했네요 :]

 

다들 문제 없이 해결하시길!

 

 

아 그리구!!!

 

modelSelected 로 구현하신분들은 ItemSelected 로 변경해주셔야해요

modelSelected 로 하면 빌드는 되는데 런타임에 크래시 날거에요 참고하시길!

저는 크래시 로그에서 제대로 안알려줘서 많이 삽질했거든요!