[SwiftUI] TaskGroup

Junyoung Park·2022년 8월 28일
0

SwiftUI

목록 보기
59/136
post-thumbnail
post-custom-banner

How to use TaskGroup to perform concurrent Tasks in Swift | Swift Concurrency #6

TaskGroup

구현 목표

  • async let을 통해 구현한 한 번에 여러 개의 비동기 패치된 데이터를 한 번에 표현하기 → 여러 개의 async let이 아니라 한 줄의 taskGroup을 통해 확장성 확보

구현 태스크

  1. TaskGroup을 통한 여러 개의 Task 실행
  2. 원하는 개수의 TaskGroup 실행
  3. 실패 가능한 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)
            }
        }
    }
}

구현 화면

  • 10개의 데이터 패치를 동적으로 결정 → 임의로 일부 데이터 패치할 URL은 에러를 throw하는 URL → 실패 케이스는 널 값으로 넘기고 성공한 패치 데이터만 리턴
profile
JUST DO IT
post-custom-banner

0개의 댓글