관리 메뉴

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

[iOS - swift] 2. 이미지 캐싱(imageCache), 이미지 효율적으로 로드 방법(tableView, collectionView): async + NSCache, pagination 본문

iOS 응용 (swift)

[iOS - swift] 2. 이미지 캐싱(imageCache), 이미지 효율적으로 로드 방법(tableView, collectionView): async + NSCache, pagination

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

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

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

cf) 애플 공식 문서에서 나온 ImageCache 방법은 여기 참고

처리 방법 2단계 

  1. scrollViewDidScroll, prefetch 방법으로 모든 페이지의 이미지를 한꺼번에 호출하지 않고 스크롤에 따라 page를 늘려나가며 API호출 > 이미지 url들을 획득
  2. 이미지 url들만 우선 cell의 모델에 적용
  3. ImageCache를 통해서 url들에 대해서 이미지 로드 > cell의 item 모델 객체에 image데이터 입력 > Diffable Data Source방법을 통해 셀 업데이트

api 예시

ImageCache 모듈 

1) 이미지 캐시 모듈과 cell에 사용될 protocol 정의

  • Diffable Data Source 방법을 사용해야 하므로 Hashable할 수 있도록 identifier프로퍼티도 선언
    • <SectionIdentifier, ItemIdentifer>에서 ItemIdentifer에 들어갈 모델
// ImageCache.swift

protocol Item {
    var image: UIImage { get set }
    var imageUrl: URL { get }
    var identifier: UUID { get }
}

 

  • ImageCompletion 정의: ImageCache의 결과값은 completion으로 사용
    • (Item, UIIMage?) -> void 에서 Item은 기존 Item이고 UIImage는 새로 얻어온 image데이터를 의미
    • 사용하는 쪽에서 저 결과값을 받아서, item.image와 UIImage를 비교하여 동일하다면 업데이트하지 않게끔 사용
// ImageCache.swift

typealias ImageCompletion = (Item, UIImage?) -> Void
  • imageCache 클래스 정의
    • prefetch image: viewController에 있는 prefetchRowAt 델리게이트 메소드에서 불릴 기능
    • load image: cache에 존재하면 그 데이터를 사용하고, 없으면 네트워크 호출을 실행
// ImageCache.swift

class ImageCache {
    let provider: Provider
    init(provider: Provider) {
        self.provider = provider
    }

    private let cache = NSCache<NSURL, UIImage>()
    private var prefetches: [UUID] = []
    private var completions: [NSURL: [ImageCompletion]] = [:]

    // Prefetch

    func prefetchImage(for item: Item) {
        let url = item.imageUrl as NSURL
        guard cachedImage(for: url) == nil, !prefetches.contains(item.identifier) else { return }
        prefetches.append(item.identifier)

        provider.request(item.imageUrl) { [weak self] result in
            switch result {
            case .success(let data):
                guard let image = UIImage(data: data) else { return }
                self?.cache.setObject(image, forKey: url)
                self?.prefetches.removeAll { $0 == item.identifier }
            default: break
            }
        }
    }

    // Load

    func loadImage(for item: Item, completion: @escaping ImageCompletion) {
        let url = item.imageUrl as NSURL
        if let image = cachedImage(for: url) {
            completion(item, image)
            return
        }

        if !completions.isEmpty, completions[url] != nil {
            completions[url]?.append(completion)
            return
        } else {
            completions[url] = [completion]
        }

        provider.request(item.imageUrl) { [weak self] result in
            switch result {
            case .success(let data):
                guard let image = UIImage(data: data) else { return }

                guard let completions = self?.completions[url] else {
                    completion(item, nil)
                    return
                }

                self?.cache.setObject(image, forKey: url)

                completions.forEach { completion in
                    completion(item, image)
                }
            case .failure(let error):
                print(error)
                completion(item, nil)
            }

            self?.completions.removeValue(forKey: url)
        }
    }

    // Reset

    func reset() {
        completions.removeAll()
        prefetches.removeAll()
        cache.removeAllObjects()
    }


    // Cache

    private func cachedImage(for url: NSURL) -> UIImage? {
        cache.object(forKey: url)
    }
}

ViewModel에서 사용

  • prefetch (ViewController의 prefetchAtRow 델리게이트 메소드에서 불리는 메소드)
// PhotoViewModel.swift

func prefetchImage(at indexPath: IndexPath) {
    guard let photo = dataSource.itemIdentifier(for: indexPath) else {
        return
    }

    imageCache.prefetchImage(for: photo)
}
  • 이미지 로드 (ViewController에서 UITableViewDiffableDataSource 클로저 블록에서 불리는 메소드)
// PhotoViewModel.swift

func loadImages(for photo: Photo) {

    imageCache.loadImage(for: photo) { [weak self] item, image in
        guard let `self` = self else { return }
        guard let photo = item as? Photo else { return }
        guard let image = image, image != photo.image else { return }

        photo.image = image
        var snapshot = `self`.dataSource.snapshot()
        guard snapshot.indexOfItem(photo) != nil else { return }

        snapshot.reloadItems([photo])
        DispatchQueue.global(qos: .background).async {
            `self`.dataSource.apply(snapshot, animatingDifferences: false)
        }
    }
}

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

 

* 참고

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

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

-Asynchronously Loading Images into Table and Collection Views: https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views

 

Comments