How to use TaskGroup to perform concurrent Tasks in Swift | Swift Concurrency #6
async let
을 통해 구현한 한 번에 여러 개의 비동기 패치된 데이터를 한 번에 표현하기 → 여러 개의 async let
이 아니라 한 줄의 taskGroup
을 통해 확장성 확보TaskGroup
을 통한 여러 개의 Task
실행TaskGroup
실행Task
를 사용한 TaskGroup
생성 func fetchImagesWithTaskGroupWithNumber(from urlString: String, number: Int) async throws -> [UIImage] {
guard let url = getURL(from: urlString) else { throw URLError(.badURL) }
return try await withThrowingTaskGroup(of: UIImage.self) { group in
// group: ThrowingTaskGroup<UIImage, Error>
var images: [UIImage] = []
for _ in 0..<number {
group.addTask {
try await self.fetchImage(from: urlString)
}
}
for try await image in group {
// Wait for each of those tasks until their results come back
images.append(image)
}
return images
}
}
import SwiftUI
protocol TaskGroupBootCampProtocol {
func fetchImage(from urlString: String) async throws -> UIImage
func fetchImagesWithAsyncLet(from urlString: String) async throws -> [UIImage]
func fetchImagesWithTaskGroup(from urlString: String) async throws -> [UIImage]
func fetchImagesWithTaskGroupWithNumber(from urlString: String, number: Int) async throws -> [UIImage]
func fetchImagesWithTaskGroupWithNumberWithOptional(from urlString: String, number: Int) async throws -> [UIImage]
}
class TaskGroupBootCampDataService: TaskGroupBootCampProtocol {
func fetchImage(from urlString: String) async throws -> UIImage {
guard let url = getURL(from: urlString) else { throw URLError(.badURL)}
do {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else { throw URLError(.badURL) }
return image
} catch {
throw error
}
}
func fetchImagesWithAsyncLet(from urlString: String) async throws -> [UIImage] {
guard let url = getURL(from: urlString) else { throw URLError(.badURL) }
async let fetchImage1 = fetchImage(from: urlString)
async let fetchImage2 = fetchImage(from: urlString)
async let fetchImage3 = fetchImage(from: urlString)
async let fetchImage4 = fetchImage(from: urlString)
let (image1, image2, image3, image4) = try await (fetchImage1, fetchImage2, fetchImage3, fetchImage4)
return [image1, image2, image3, image4]
}
func fetchImagesWithTaskGroup(from urlString: String) async throws -> [UIImage] {
guard let url = getURL(from: urlString) else { throw URLError(.badURL) }
return try await withThrowingTaskGroup(of: UIImage.self) { group in
// group: ThrowingTaskGroup<UIImage, Error>
var images: [UIImage] = []
group.addTask {
try await self.fetchImage(from: urlString)
}
group.addTask {
try await self.fetchImage(from: urlString)
}
group.addTask {
try await self.fetchImage(from: urlString)
}
group.addTask {
try await self.fetchImage(from: urlString)
}
for try await image in group {
// Wait for each of those tasks until their results come back
images.append(image)
}
return images
}
}
func fetchImagesWithTaskGroupWithNumber(from urlString: String, number: Int) async throws -> [UIImage] {
guard let url = getURL(from: urlString) else { throw URLError(.badURL) }
return try await withThrowingTaskGroup(of: UIImage.self) { group in
// group: ThrowingTaskGroup<UIImage, Error>
var images: [UIImage] = []
for _ in 0..<number {
group.addTask {
try await self.fetchImage(from: urlString)
}
}
for try await image in group {
// Wait for each of those tasks until their results come back
images.append(image)
}
return images
}
}
func fetchImagesWithTaskGroupWithNumberWithOptional(from urlString: String, number: Int) async throws -> [UIImage] {
guard let url = getURL(from: urlString) else { throw URLError(.badURL) }
return try await withThrowingTaskGroup(of: UIImage?.self) { group in
// group: ThrowingTaskGroup<UIImage, Error>
var images: [UIImage] = []
// bacURL Error
group.addTask {
try? await self.fetchImage(from: "")
}
for _ in 1..<number {
group.addTask {
try? await self.fetchImage(from: urlString)
}
}
for try await image in group {
// Wait for each of those tasks until their results come back
// if successfully fetched from network, then works.
// otherwise, ignore it.
if let image = image {
images.append(image)
}
}
return images
}
}
private func getURL(from urlString: String) -> URL? {
guard let url = URL(string: urlString) else { return nil }
return url
}
}
async let
을 사용하는 방법 존재async let
은 패치할 데이터의 개수마다 달라져야 하기 때문에 동적으로 받아들이기 힘듦TaskGroup
생성 → 에러를 throw
할 수 있는 태스크 그룹 또는 일반 태스크 그룹 선택 → 어떤 타입을 리턴할 것인지 결정 → 패치할 작업을 태스크에 추가(group.addTask
)throw
하기 때문에 다운로드 성공한 데이터는 패치할 수 없음 → 옵셔널 try
를 통해 에러 throw
가 아니라 널 값 할당, 이후 옵셔널 바인딩을 통해 '다운로드' 확실한 데이터만 리턴class TaskGroupBootCampViewModel: ObservableObject {
@Published var images: [UIImage] = []
let dataService: TaskGroupBootCampProtocol
let urlString: String
init(dataService: TaskGroupBootCampProtocol, urlString: String) {
self.dataService = dataService
self.urlString = urlString
}
func fetchImage() async {
do {
let image = try await dataService.fetchImage(from: urlString)
DispatchQueue.main.async {
self.images.append(image)
}
} catch {
print(error.localizedDescription)
}
}
func fetchImagesWithAsync() async {
do {
let images = try await dataService.fetchImagesWithAsyncLet(from: urlString)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.images.append(contentsOf: images)
}
} catch {
print(error.localizedDescription)
}
}
func fetchImagesWithTaskGroup() async {
do {
let images = try await dataService.fetchImagesWithTaskGroup(from: urlString)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.images.append(contentsOf: images)
}
} catch {
print(error.localizedDescription)
}
}
func fetchImagesWithTaskGroupWithNumber(number: Int) async {
do {
let images = try await dataService.fetchImagesWithTaskGroupWithNumber(from: urlString, number: number)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.images.append(contentsOf: images)
}
} catch {
print(error.localizedDescription)
}
}
func fetchImagesWithTaskGroupWithNumberWithOptional(number: Int) async {
do {
let images = try await dataService.fetchImagesWithTaskGroupWithNumberWithOptional(from: urlString, number: number)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.images.append(contentsOf: images)
}
} catch {
print(error.localizedDescription)
}
}
}
struct TaskGroupBootCamp: View {
@StateObject private var viewModel: TaskGroupBootCampViewModel
let columns = [GridItem(.flexible()), GridItem(.flexible())]
init(dataService: TaskGroupBootCampProtocol, urlString: String) {
_viewModel = StateObject(wrappedValue: TaskGroupBootCampViewModel(dataService: dataService, urlString: urlString))
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(viewModel.images, id:\.self) {
image in
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(height: 100)
}
}
}
.navigationTitle("TaskGroupBootCamp")
.task {
await viewModel.fetchImagesWithTaskGroupWithNumberWithOptional(number: 10)
}
}
}
}
throw
하는 URL → 실패 케이스는 널 값으로 넘기고 성공한 패치 데이터만 리턴