Bibi's DevLog ๐Ÿค“๐ŸŽ

[Swift] URL์—์„œ ๋น„๋™๊ธฐ๋กœ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ๋ฐ ์บ์‹ฑํ•˜๊ธฐ - NSCache ๋ณธ๋ฌธ

๐Ÿ“ฑ๐ŸŽ iOS/Code Templates

[Swift] URL์—์„œ ๋น„๋™๊ธฐ๋กœ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ๋ฐ ์บ์‹ฑํ•˜๊ธฐ - NSCache

๋น„๋น„ bibi 2022. 5. 18. 11:53

์ฐธ๊ณ ํ•œ ๊ณต์‹ ๋ฌธ์„œ : https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views
ํ•ด์„ : ๋น„๋™๊ธฐ์ ์œผ๋กœ ํ…Œ์ด๋ธ”๋ทฐ, ์ปฌ๋ ‰์…˜๋ทฐ์˜ ์ด๋ฏธ์ง€ ๋กœ๋“œํ•˜๊ธฐ Asynchronously Loading Images into Table and Collection Views

UICollectionView๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉฐ ์…€๋งˆ๋‹ค ์ด๋ฏธ์ง€๋ฅผ API์—์„œ ๋ฐ›์•„์˜ค๋Š” ์ž‘์—…์„ ํ–ˆ๋‹ค.
์Šคํฌ๋กค์ด ๋  ๋•Œ๋งˆ๋‹ค ๊ฐ™์€ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ์š”์ฒญํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด ๋•Œ API์— ์—ฌ๋Ÿฌ ๋ฒˆ ๋ฐ˜๋ณตํ•ด์„œ ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ๋˜๋ฏ€๋กœ ๊ฐ™์€ ๋™์ž‘์„ ์—ฌ๋Ÿฌ ๋ฒˆ ํ•˜๋Š” ๋‚ญ๋น„์ด๊ธฐ๋„ ํ•˜๊ณ , ์ตœ์•…์˜ ๊ฒฝ์šฐ API์—์„œ ๋ฐด์„ ๋‹นํ•  ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์ด๋ฏธ์ง€ ์บ์‹ฑ์˜ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด๊ฒŒ ๋˜์—ˆ๋‹ค.

ImageCacheManager

์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ URL์„ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ๋‹ค์šด๋ฐ›๋Š” ์ž‘์—…์„ ํ•จ

  • ํด๋กœ์ €๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ : ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ ๊นŒ์ง€ ์•ฑ์ด ๋ฉˆ์ถฐ์žˆ๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€
  • NSCache๋ฅผ ํ™œ์šฉํ•ด ์ด๋ฏธ์ง€ ์บ์‹ฑ : ์ด๋ฏธ์ง€ ์š”์ฒญ์ด ์™”์„ ๋•Œ ์บ์‹œ์— ์—†์œผ๋ฉด ๋‹ค์šด๋กœ๋“œ, ์บ์‹œ์— ์žˆ์œผ๋ฉด ์บ์‹ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๋ฆฌํ„ด
  • ๊ฐ™์€ ์ด๋ฏธ์ง€์— ๋Œ€ํ•œ ๋ฐ˜๋ณต์ ์ธ ์š”์ฒญ : ๋”•์…”๋„ˆ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด URL์— ๋Œ€์‘๋˜๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•ด ๋‘๊ณ , ๊ฐ™์€ ์ด๋ฏธ์ง€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ ์š”์ฒญ์— ํ•œ๊บผ๋ฒˆ์— ์ด๋ฏธ์ง€ ๋ฆฌํ„ด

ImageCacheManager

//
//  ImageCache.swift
//  starbuckst
//
//  Created by Bibi on 2022/05/16.
//

import UIKit
import Foundation
public class ImageCacheManager {

    public static let publicCacheManager = ImageCacheManager() // ์‹ฑ๊ธ€ํ†ค?
    // ์‚ฌ์šฉ ์‹œ ImageCache.publicCacheManager.load()์™€ ๊ฐ™์ด ์‚ฌ์šฉ..

    // URL์— ๋Œ€ํ•œ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๋Š” ์บ์‹œ ํ”„๋กœํผํ‹ฐ
    private let cachedImages = NSCache<NSURL, UIImage>()  // key๊ฐ’์œผ๋กœ ํด๋ž˜์Šค๋ฅผ ์š”๊ตฌํ•˜๋ฏ€๋กœ URL๋Œ€์‹  NSURL์‚ฌ์šฉ

    // ์บ์‹œ์— ์ด๋ฏธ์ง€๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, urlSession์„ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ์–ป์–ด์˜ค๊ธฐ ์œ„ํ•ด response๋ฅผ ๋ฐ›์€ ํ›„ ๊ฒฐ๊ณผ๊ฐ’์„ ์ „๋‹ฌ๋ฐ›๊ธฐ ์œ„ํ•ด ์„ ์–ธํ•œ ๋”•์…”๋„ˆ๋ฆฌ
    private var loadingResponses = [NSURL: [(ImageItem, UIImage?) -> Swift.Void]]()

// URL์„ ์ธ์ˆ˜๋กœ ๋ฐ›์•„ ์บ์‹œ๋œ ์ด๋ฏธ์ง€๋ฅผ ํš๋“ํ•˜๋Š” ๋ฉ”์„œ๋“œ
    private final func getCachedImage(url: NSURL) -> UIImage? {
        return cachedImages.object(forKey: url)
    }

// ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์บ์‹ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œ ๋ฐ ์บ์‹œํ•ฉ๋‹ˆ๋‹ค.
// image(url:)์˜ ๊ฒฐ๊ณผ๋กœ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ทธ ์ด๋ฏธ์ง€๋ฅผ completion์— ์ „๋‹ฌ, ์—†์œผ๋ฉด URLSession์„ ํ†ตํ•ด ๋ฐ›์•„์™€์„œ completion์— ์ „๋‹ฌ
    final func loadImage(url: NSURL, imageItem: ImageItem, completion: @escaping (ImageItem, UIImage?) -> Swift.Void) {
        // ํ•ด๋‹น ์ด๋ฏธ์ง€๊ฐ€ ์บ์‹œ๋˜์–ด ์žˆ๋‹ค๋ฉด ์ฐพ์€ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
        if let cachedImage = getCachedImage(url: url) {
            DispatchQueue.main.async { // ์™œ ๋น„๋™๊ธฐ?
                completion(imageItem, cachedImage)
            }
            return
        }

        if loadingResponses[url] != nil { // ํ•ด๋‹น ์ด๋ฏธ์ง€์— ๋Œ€ํ•ด ๋‘˜ ์ด์ƒ์˜ ์š”์ฒญ์ด ์žˆ๋Š” ๊ฒฝ์šฐ, loadingResponses์— completion ๋ธ”๋ก์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
            loadingResponses[url]?.append(completion)
            return
        } else { // ํ•ด๋‹น ์ด๋ฏธ์ง€์— ๋Œ€ํ•ด ์ฒซ ์š”์ฒญ์ธ ๊ฒฝ์šฐ loadingResponses์— completion ๋“ฑ๋ก
            loadingResponses[url] = [completion]
        }

        // (URL๋กœ๋ถ€ํ„ฐ) ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.
        // URLSession(.ephimeral) : NSCache๋ฅผ ๋”ฐ๋กœ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ URLSession์˜ ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•œ ์„ค์ •
        let urlSession = URLSession(configuration: URLSessionConfiguration.ephemeral)
        urlSession.dataTask(with: url as URL) { (data, response, error) in
            // error์™€ data๋ฅผ ์ฒดํฌํ•˜๊ณ  ์ด๋ฏธ์ง€ ์ƒ์„ฑ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
            guard let responseData = data,
                  let image = UIImage(data: responseData),
                  let responseBlocks = self.loadingResponses[url],
                  error == nil else { // ๋„ท ์ค‘์— ํ•˜๋‚˜๋ผ๋„ nil์ด๋ฉด completion์œผ๋กœ nil์„ ์ „๋‹ฌํ•˜๋ฉฐ ๋ฆฌํ„ด
                DispatchQueue.main.async {
                    completion(imageItem, nil)
                }
                return
            }
            // ๋ฐ›์•„์˜จ ์ด๋ฏธ์ง€๋ฅผ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
            self.cachedImages.setObject(image, forKey: url, cost: responseData.count)
            // ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ ์š”์ฒญ๋“ค์— ๋Œ€ํ•ด ๋ฐ˜๋ณตํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
            for block in responseBlocks {
                DispatchQueue.main.async {
                    block(imageItem, image)
                }
                return
            }
        }.resume()
    }
}

ImageItem

์ด๋ฏธ์ง€์˜ UIImage, URL, id๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฐ์ฒด

//
//  ImageItem.swift
//  starbuckst
//
//  Created by Bibi on 2022/05/17.
//

import Foundation
import UIKit

class ImageItem { // struct?

    var image: UIImage? = nil // ์ด๋ฏธ์ง€
    let url: URL // ์ด๋ฏธ์ง€URL
    let identifier = UUID() // ์ด๋ฏธ์ง€์˜ ID

    init(image: UIImage? = nil, url: URL) {
        self.image = image
        self.url = url
    }
}

ViewController

๋ทฐ์ปจ์—์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉ

imageCacheManager.loadImage(url: mainImageURL as NSURL, imageItem: mainImageItem) { (imageItem, image) in
    DispatchQueue.main.async {
       self.mainEventView.image = image
       self.mainEventView.setNeedsDisplay()
    }
}