관리 메뉴

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

[iOS - swift] 1. 이미지 캐싱, 이미지 효율적으로 로드 방법, 스크롤에 따라 이미지 로드(tableView, collectionView): scrollViewDidScroll, prefetch, pagination (페이지네이션) 본문

iOS 응용 (swift)

[iOS - swift] 1. 이미지 캐싱, 이미지 효율적으로 로드 방법, 스크롤에 따라 이미지 로드(tableView, collectionView): scrollViewDidScroll, prefetch, pagination (페이지네이션)

jake-kim 2021. 10. 2. 23:14

1. 이미지 캐싱, 이미지 효율적으로 로드 방법, 스크롤에 따라 이미지 로드(tableView, collectionView): scrollViewDidScroll, prefetch

2. 이미지 캐싱, 이미지 효율적으로 로드 방법(tableView, collectionView): async + NSCache

아이디어

  • 스크롤이 느려지고, 앱이 꺼지고 이미지들이 사용자에게 보여질 때 느려질 수 있으므로 캐시를 사용
  • 비동기적으로 이미지를 저장하고 cache를 사용

어떻게?

  • request(page:1) API 호출 > 10개의 이미지 url 획득
  • 10개의 이미지 url만 cell의 model에 먼저 저장 (image는 따로 호출하여 반영)
  • image는 최초 10개만 일단 로드하고, 나머지는 아래에서 나오는 내용인 스크롤에 따라 호출하는 로직 사용

이미지 캐싱 or preFetch

  • UITableView에서 서버에서 많은 리스트 데이터를 받아올 때 한꺼번에 받아올 경우 device와 서버에 부담이 생기므로 paging별로 쪼개어서 받아오는 테크닉 2가지 방법 존재 > 2가지 모두 사용하여 구현
    • 1) scrollViewDidScroll(_:)
    • 2) preFetchRow

1) scrollViewDidScroll(_:)

  • UITableView가 상속하고 있는 UIScrollView의 delegate인 scrollViewDidScroll(_:) 사용
  • 스크롤을 아래로 내릴 때 더 이상 내려갈 곳이 없는 경우, tableView에 addData
  • Scroll을 내릴 때 약간의 딜레이가 있는 것처럼 느껴지는 단점 존재

* 실제로는 offset값과 inset값도 고려할것

// 참고

extension ViewController: UICollectionViewDelegate {
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // contentOffset: 스크롤한 길이
    // contentInset: collectionView의 테두리 부분과의 여백 (4곳만 존재)
    // contentSize: 스크롤 가능한 콘텐츠 사이즈 (주의 - contentInset 값을 합해야, collectionView 전체 콘텐트 사이즈)
    
    let offset = scrollView.contentOffset.x + scrollView.contentInset.left
    let width = scrollView.contentSize.width + scrollView.contentInset.left + scrollView.contentInset.right
    
    let scrollRatio = offset / width
  }
}
  • 코드: TableView는 UIScrollView의 서브클래스이므로 scrollViewDidScroll를 구현
class ViewController: UIViewController {
	...
    private func execute() {
        currentPage += 1

        let photoListRequestDTO = PhotoListRequestDTO(page: currentPage)
        let endpoint = APIEndpoints.getPhotosInfo(with: photoListRequestDTO)
        provider?.request(with: endpoint, completion: { [weak self] result in
            switch result {
            case .success(let responseDTO):
                print(responseDTO)
                self?.appendPhotos(with: responseDTO)
            case .failure(let error):
                print(error)
            }
        })
    }
}

extension ViewController {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let contentHeight = scrollView.contentSize.height
        let yOffset = scrollView.contentOffset.y
        let heightRemainBottomHeight = contentHeight - yOffset

        let frameHeight = scrollView.frame.size.height
        if heightRemainBottomHeight < frameHeight {
            execute()
        }
    }
}
  • 주의: 단, scrollViewDidScroll에서 여러번 호출될수 있으므로, API를 호출하기 전에 flag값을 놓고, api가 호출이 끝난 경우만 동작하도록 설정 필요
enum ViewState {
    case idle
    case isLoading
}

var viewState = ViewState.idle
func execute() {
	guard viewState == .idle else { return }
    
    viewState = isLoading
    API.request(...) { result in
    	self.viewState = idle
    }
}

2) prefetchRow

  • 아이디어: indexPaths에 해당되는 셀에 필요한 데이터를 미리 받아오는 메소드 (아직 보이지 않는 셀들을 미리 시스템에서 구분하여 델리게이트 동작)

  • iOS 10+에서 가능한 기능
  • prefetchDataSource = self로 위임, prefetchRowsAt 구현
class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.prefetchDataSource = self
    }
}

// MARK: - UITableViewDataSourcePrefetching
extension ViewController: UITableViewDataSourcePrefetching {
    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        indexPaths.forEach { viewModel.prefetchImage(at: $0) }
    }
}
  • collectionView에서의 refetch delegate
// UICollectionViewDataSourcePrefetching
collectionView.prefetchDataSource = self

func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
    // Begin asynchronously fetching data for the requested index paths.
    for indexPath in indexPaths {
        let model = models[indexPath.row]
        asyncFetcher.fetchAsync(model.identifier)
    }
}

예제 - scroll에 따라 사진 로드

  • MVVM 구조 내용
    • ViewController에서 UITableViewDiffableDataSource 클로저 저장
    • ViewModel에서 데이터가 변경됨에 따라 snapshot을 남기면 ViewController에서 cell업데이트 진행
    • scroll에 따라 이미지를 로드해야하므로 ViewController에 ScrollViewDidScroll 델리게이트 구현
      > viewModel.loadData() 호출 > viewModel에서 snapshot하여 UITableViewDiffableDataSource클로저 블록에 셀 업데이트 요청
    • prefetch되어야 하므로, ViewController에서 prefetchAt 델리게이트 구현
      > viewModel.loadImage() 호출 > viewModel에서 snapshot하여 UITableViewDiffableDataSource클로저 블록에 셀 업데이트 요청

* 전체 소스코드: https://github.com/JK0369/PaginationExample

 

* 참고

- tableView(_:prefetchRowsAt:): https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching/1771764-tableview

- prefetch: 

https://andreygordeev.com/2017/02/20/uitableview-prefetching/

https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching/prefetching_collection_view_data

Comments