๋น๋๊ธฐ์ ์ผ๋ก ํ ์ด๋ธ๋ทฐ, ์ปฌ๋ ์ ๋ทฐ์ ์ด๋ฏธ์ง ๋ก๋ํ๊ธฐ Asynchronously Loading Images into Table and Collection 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