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

๋น„๋™๊ธฐ์ ์œผ๋กœ ํ…Œ์ด๋ธ”๋ทฐ, ์ปฌ๋ ‰์…˜๋ทฐ์˜ ์ด๋ฏธ์ง€ ๋กœ๋“œํ•˜๊ธฐ Asynchronously Loading Images into Table and Collection Views ๋ณธ๋ฌธ

๐Ÿ“ฑ๐ŸŽ iOS/๐Ÿ Apple Developer Documentation

๋น„๋™๊ธฐ์ ์œผ๋กœ ํ…Œ์ด๋ธ”๋ทฐ, ์ปฌ๋ ‰์…˜๋ทฐ์˜ ์ด๋ฏธ์ง€ ๋กœ๋“œํ•˜๊ธฐ Asynchronously Loading Images into Table and Collection Views

๋น„๋น„ bibi 2022. 5. 16. 17:57

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

๊ฐœ์š”

์ด๋ฏธ์ง€ ์บ์‹ฑ์€ ๋‹น์‹ ์˜ ์•ฑ์˜ ํ…Œ์ด๋ธ”๋ทฐ์™€ ์ปฌ๋ ‰์…˜๋ทฐ๊ฐ€ ์ธ์Šคํ„ด์Šค๋ฅผ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค๊ณ  ์Šคํฌ๋กค์— ๋น ๋ฅด๊ฒŒ ์‘๋‹ตํ•˜๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค. ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ์˜ ์•ฑ์€ URL์„ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ์‹œ์—ฐํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋“ค์€ assets catalog์˜ ์ผ๋ถ€๊ฐ€ ์•„๋‹Œ, URL์„ ํ†ตํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฐ๊ฐ ๋กœ๋”ฉ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ธฐ ์œ„ํ•œ ์•ฑ bundle์˜ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์œ ์ € ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๊ณ„์† ๋ฐ˜์‘ํ•˜๋„๋ก ๋ณด์žฅํ•ด ์ค๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์ ํŠธ๋Š” Mac Catalyst ๋˜ํ•œ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ๋กœ๋”ฉ๊ณผ ์บ์‹ฑ ๋‹ค๋ฃจ๊ธฐ

์ด ์˜ˆ์‹œ์—์„œ, ImageCache.swift ํด๋ž˜์Šค๋Š” URLSession์œผ๋กœ URL์„ ํ†ตํ•œ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ์™€ NSCache๋ฅผ ํ†ตํ•œ ์ด๋ฏธ์ง€ ์บ์‹ฑ์˜ ๊ธฐ๋ณธ ๋งค์ปค๋‹ˆ์ฆ˜์„ ๋ณด์—ฌ ์ค๋‹ˆ๋‹ค. UITableView์™€ UICollectionView ๊ฐ™์€ ๋ทฐ๋“ค์€ UIScrollView์˜ ์„œ๋ธŒํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

์œ ์ €๊ฐ€ ๋ทฐ์—์„œ ์Šคํฌ๋กค์„ ํ•  ๋•Œ, ์•ฑ์€ ๊ฐ™์€ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ˆ์‹œ๋Š” ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋  ๋•Œ ๊นŒ์ง€ ๊ด€๋ จ๋œ completion block์„ ์œ ์ง€ํ•˜๋‹ค๊ฐ€, ์š”์ฒญํ•œ ๋ชจ๋“  block์— ์ด๋ฏธ์ง€๋ฅผ ์ „๋‹ฌํ•จ์œผ๋กœ์จ, API๊ฐ€ ์ฃผ์–ด์ง„ URL์— ๋Œ€ํ•ด ํ•œ ๋ฒˆ๋งŒ ์ด๋ฏธ์ง€๋ฅผ ์š”์ฒญํ•˜๋„๋ก ํ•ด ์ค๋‹ˆ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ๊ฐ€ ๊ธฐ๋ณธ ์บ์‹ฑ ๋ฐ ๋กœ๋”ฉ ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š”์ง€๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

// ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์บ์‹ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œ ๋ฐ ์บ์‹œํ•ฉ๋‹ˆ๋‹ค.
final func load(url: NSURL, item: Item, completion: @escaping (Item, UIImage?) -> Swift.Void) {
    // ์บ์‹œ๋œ ์ด๋ฏธ์ง€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
    if let cachedImage = image(url: url) {
        DispatchQueue.main.async {
            completion(item, cachedImage)
        }
        return
    }
    // ํ•ด๋‹น ์ด๋ฏธ์ง€์— ๋Œ€ํ•ด ๋‘˜ ์ด์ƒ์˜ ์š”์ฒญ์ž๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, completion ๋ธ”๋ก์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    if loadingResponses[url] != nil {
        loadingResponses[url]?.append(completion)
        return
    } else {
        loadingResponses[url] = [completion]
    }
    // ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
    ImageURLProtocol.urlSession().dataTask(with: url as URL) { (data, response, error) in
        // error์™€ data๋ฅผ ํ™•์ธํ•˜๊ณ , ์ด๋ฏธ์ง€ ์ƒ์„ฑ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
        guard let responseData = data, let image = UIImage(data: responseData),
            let blocks = self.loadingResponses[url], error == nil else {
            DispatchQueue.main.async {
                completion(item, nil)
            }
            return
        }
        // ์ด๋ฏธ์ง€๋ฅผ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
        self.cachedImages.setObject(image, forKey: url, cost: responseData.count)
        // ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ ์š”์ฒญ์ž์— ๋Œ€ํ•ด ๋ฐ˜๋ณตํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
        for block in blocks {
            DispatchQueue.main.async {
                block(item, image)
            }
            return
        }
    }.resume()
}

์ฑ…์ž„๊ฐ ์žˆ๊ฒŒ ๋ฐ์ดํ„ฐ์†Œ์Šค ์—…๋ฐ์ดํŠธํ•˜๊ธฐ?

์‹คํ–‰ ์‹œ์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ์•ฑ์€ ์™„๋ฃŒ๊นŒ์ง€ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์—, ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ์ด๋‚˜ ์ข…๋ฃŒ๋  ์œ„ํ—˜์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์•ฑ์ด ์‹คํ–‰๋˜๊ธฐ ์ „์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ๋ฅผ ์š”๊ตฌํ•˜์ง€ ์•Š๋Š” ์ด์ƒ, UI๊ฐ€ ์š”์ฒญํ•  ๋•Œ๋งŒ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•˜์‹ญ์‹œ์˜ค.

Note

ํ™”๋ฉด์— ๋ณด์—ฌ์ง€๊ธฐ ์ „ ํ•ญ๋ชฉ๋“ค์„ ํ™•์‹คํžˆ ๋กœ๋“œํ•˜๊ธฐ ์œ„ํ•ด์„œ, ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด prefetching API๋“ค์„ ์‚ฌ์šฉํ•ด ๋ณด์‹ญ์‹œ์˜ค. ๋ฐ์ดํ„ฐ์˜ prefetching์˜ ์ข‹์€ ์˜ˆ์‹œ๋Š” Prefetching Collection View Data๋ฅผ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.

์ผ๋ฐ˜์ ์œผ๋กœ ์•ฑ์€ ๋ฐ์ดํ„ฐ์†Œ์Šค๊ฐ€ ์…€์ด ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์„ค์ •ํ•  ๋•Œ ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ทฐ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๋ณด์—ฌ์ฃผ๋Š” ํ•˜๋‚˜์˜ ์ ‘๊ทผ์„ ์‹œ์—ฐํ•ฉ๋‹ˆ๋‹ค:

var content = cell.defaultContentConfiguration()
content.image = item.image
ImageCache.publicCache.load(url: item.url as NSURL, item: item) { (fetchedItem, image) in
    if let img = image, img != fetchedItem.image {
        var updatedSnapshot = self.dataSource.snapshot()
        if let datasourceIndex = updatedSnapshot.indexOfItem(fetchedItem) {
            let item = self.imageObjects[datasourceIndex]
            item.image = img
            updatedSnapshot.reloadItems([item])
            self.dataSource.apply(updatedSnapshot, animatingDifferences: true)
        }
    }
}
cell.contentConfiguration = content