Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] RxDataSources를 사용한 PrefetchItems, Pagination (페이지네이션) 본문

iOS 응용 (swift)

[iOS - swift] RxDataSources를 사용한 PrefetchItems, Pagination (페이지네이션)

jake-kim 2021. 12. 19. 19:08

Pagination과 PrefetchItem 적용 -> 매끄러운 이미지 로딩

사용한 기초 프레임워크 참고

사용 API

  • Unsplash API
  • page별로, 랜덤 이미지를 로드하는 API

PrefetchItems

  • tableView, collectionVIew와 같이 ScrollView의 스크롤할때 아직 화면에서 보이지 않지만 그 다음 보여야하는 cell에 관한 정보를 미리 얻어오는 것
  • 정보를 미리 얻어와서, 불러와야할 이미지 url을 알고 스크롤 하기전에 prefetchItems 이벤트가 발생할때 미리 로딩하는 것

Pagination

  • API 호출 시 page정보를 가지고 있어서, 정보를 한꺼번에 가져오지 않고 page=1, page=2, page=3와 같이 page별로 쪼개서 API호출하고 이미지를 업데이트 시키는 과정
  • 전체 content의 높이(contentSize.height), 현재 스크롤된 위치(contentOffset.y) 값을 가지고, 본래의 사이즈보다 작은 경우 데이터를 맨 아래로 내리기 전에 호출
    • 구체적인 개념은 이곳 참고

PrefetchItems 구현 방법

  • ViewController에서는 prefetchItems 이벤트를 받는 처리 추가
    • collectionView.rx.prefetchItems 사용
    • items 정보 획득 가능
// PhotoViewController

self.photoCollectionView.rx.prefetchItems
  .throttle(.seconds(1), scheduler: MainScheduler.asyncInstance)
  .observe(on: MainScheduler.asyncInstance)
  .asObservable()
  .map(dataSource.items(at:))
  .map(Reactor.Action.prefetchItems)
  .bind(to: reactor.action)
  .disposed(by: disposeBag)
  • reactor쪽에서는 prefetchItems을 받아서, Kingfisher를 통해 해당 url에 관한 image를 미리 캐싱해놓는 작업 진행
    • 캐싱은 ImagePrefetcher(resource:).start()로 사용
// PhotoViewReactor.swift

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  ...
  case .prefetchItems(let items):
    var urls = [URL]()
    items.forEach {
      if case let .main(photo) = $0,
         let url = URL(string: photo.urlString) {
        urls.append(url)
      }
    }
    ImagePrefetcher(resources: urls).start() // <- 캐싱
    return .empty()
}

Pagination 구현 방법

  • pagination을 구현하기 전에 UIImageView에 Kingfisher를 통해 캐싱하는 코드를 extension으로 추가
    import UIKit
    import Kingfisher
    
    extension UIImageView {
      func setImage(with urlString: String) {
        ImageCache.default.retrieveImage(forKey: urlString, options: nil) { result in
          switch result {
          case .success(let value):
            if let image = value.image {
              //캐시가 존재하는 경우
              self.image = image
            } else {
              //캐시가 존재하지 않는 경우
              guard let url = URL(string: urlString) else { return }
              let resource = ImageResource(downloadURL: url, cacheKey: urlString)
              self.kf.indicatorType = .activity
              self.kf.setImage(
                with: resource,
                options: [
                  .transition(.fade(1.2)),
                  .forceTransition
                ]
              )
            }
          case .failure(let error):
            print(error)
          }
        }
      }
    }​
  • Pagination을 처리하기 위해 PhotoViewController에서 scroll될때마다 pagination처리에 필요한 값들을 reactor에게 넘기도록 코드 추가
    // PhotoViewController.swift
    
    self.photoCollectionView.rx.didScroll
      .withLatestFrom(self.photoCollectionView.rx.contentOffset)
      .map { [weak self] in
        Reactor.Action.pagination(
          contentHeight: self?.photoCollectionView.contentSize.height ?? 0,
          contentOffsetY: $0.y,
          scrollViewHeight: UIScreen.main.bounds.height
        )
      }
      .bind(to: reactor.action)
      .disposed(by: disposeBag)​
  • Reactor에서 해당 값을 받아서 pagination을 진행할지 확인
    • pagination 진행할지 확인하는 개념은 이곳 참고
      (아래 코드에서는 mutate(action:) 함수, .pagination 케이스에서 진행
      // PhotoViewReactor.swift
      
      class PhotoViewReactor: Reactor {
        enum Action {
          ...
          case pagination(
            contentHeight: CGFloat,
            contentOffsetY: CGFloat,
            scrollViewHeight: CGFloat
          )
          
          ...
          
        func mutate(action: Action) -> Observable<Mutation> {
          switch action {
      		...
          case let .pagination(contentHeight, contentOffsetY, scrollViewHeight):
            let paddingSpace = contentHeight - contentOffsetY
            if paddingSpace < scrollViewHeight {
              return getPhotos()
            } else {
              return .empty()
            }
          }
          
        private func getPhotos() -> Observable<Mutation> {
          self.currentPage += 1
          let photoRequest = PhotoRequest(page: currentPage)
          return self.provider.photoService.getPhotos(photoRequest: photoRequest)
            .map { (photos: [Photo]) -> [PhotoSection.Item] in
              let photoSectionItem = photos.map(PhotoSection.Item.main)
              return photoSectionItem
            }
            .map(Mutation.updateDataSource)
        }
        
        func reduce(state: State, mutation: Mutation) -> State {
          var state = state
          switch mutation {
          case .updateDataSource(let sectionItem):
            state.photoSection.items.append(contentsOf: sectionItem)
          }
          return state
        }    
      }

* 모든 소스 코드: https://github.com/JK0369/ExPaginationWithRxDataSources

Comments