func fetchThumbnails(for ids: [String]) async throws [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
for id in ids {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request)
try validateResponse(response)
guard let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: thumbSize) else {
throw ThumbnailFailedError()
}
thumbnails[id] = image
}
return thumbnails
}
위에서는 이미지를 순차적으로 처리하고 있는데, 이상적이지 않다. 동시에 다운로드 받게 하려면 어떻게 해야할까? → Task를 이용하면 된다.
Swift가 제공하는 task(유연성과 심플함 사이에서의 균형)
async let
// 위에서 이미지 가져오는 코드를 별도로 추출
// async let으로 동시 실행
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
async let (data, _) = URLSession.shared.data(for: imageReq)
async let (metadata, _) = URLSession.shared.data(for: metadataReq)
guard
let size = parseSize(from: try await metadata),
let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
else {
throw ThumbnailFailedError()
}
return image
}
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
for id in ids {
try Task.checkCancellation() // 취소 상태면 throw
if Task.isCancelled { break } // 취소면 true. 부분적인 결과만 받아볼 수도 있다는 점에서 유의해야 한다.
thumbnails[id] = try await fetchOneThumbnail(withID: id)
}
}
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
try await withThrowingTaskGroup(of: Void.self) { group in
for id in ids {
group.async {
thumbnails[id] = try await fetchOneThumbnail(withID: id) // 여기서 에러가 난다.
}
}
}
return thumbnails
}
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
for id in ids {
group.async {
return (id, try await fetchOneThumbnail(withID: id))
}
}
for try await (id, thumbnail) in group {
thumbnails[id] = thumbnail
}
}
return thumbnails
}
왜 필요한가? → sturcturecd 모델이 맞지 않는 경우가 있기 때문
예시
@MainActor
class MyDelegate: UICollectionViewDelegate {
func collectionView(_ view: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
let thumbnails = await fetchThumbnails(for: ids) // 'await' in a function that does not support concurrency
display(thumbnails, in: cell)
}
돌아가도록 수정하면 다음과 같다.
@MainActor
class MyDelegate: UICollectionViewDelegate {
func collectionView(_ view: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
async {
let thumbnails = await fetchThumbnails(for: ids) // 'await' in a function that does not support concurrency
display(thumbnails, in: cell)
}
}
unstructured tasks의 특징
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task.Handle<Void, Never>] = [:]
func collectionView(_ view: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = async {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
display(thumbnails, in: cell)
}
func collectionView(_ view: UICollectionView,
didEndDisplay cell: UICollectionViewCell,
forItemAt item: IndexPath) {
thumbnailTasks[item]?.cancel()
}
}
Detached Context
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task.Handle<Void, Never>] = [:]
func collectionView(_ view: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = async {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
asyncDetached(priority: .background) {
withTaskGroup(of: Void.self) { g in
g.async { writeToLocalCache(thumbnails) }
g.async { log(thumbnails) }
g.async { ... }
}
}
display(thumbnails, in: cell)
}
func collectionView(_ view: UICollectionView,
didEndDisplay cell: UICollectionViewCell,
forItemAt item: IndexPath) {
thumbnailTasks[item]?.cancel()
}
}
정리