
Download images with Async/Await, @escaping, and Combine | Swift Concurrency #2

Combine 프레임워크를 통해 비동기적 데이터를 다운로드한다.async/await 구문을 통해 비동기적 데이터를 다운로드한다.DispatchQueue 또는 MainActor를 사용한다.URLSession의 데이터를 다운로드하는 컴플리션 핸들러URLSession의 데이터 퍼블리셔를 구독URLSession의 데이터를 await하는 async 함수import SwiftUI
import Combine
class DownloadImageAsyncBootCampDataService {
    let urlString: String
    init(urlString: String) {
        self.urlString = urlString
    }
    
    func getURL() -> URL? {
        guard let url = URL(string: urlString) else { return nil }
        return url
    }
    
    func downloadWithEscaping(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> ()) {
        guard let url = getURL() else { return }
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard
                let data = data,
                let image = UIImage(data: data),
                let response = response as? HTTPURLResponse,
                response.statusCode >= 200 && response.statusCode < 300 else {
                completionHandler(nil, error)
                return
            }
            completionHandler(image, nil)
        }
        .resume()
    }
    
    func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
        guard
            let data = data,
            let image = UIImage(data: data),
            let response = response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else {
            return nil
        }
        return image
    }
    
    func downloadWithEscaping2(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> ()) {
        guard let url = getURL() else { return }
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let self = self else { return }
            let image = self.handleResponse(data: data, response: response)
            completionHandler(image, error)
        }
        .resume()
    }
    
    func downloadWithCombine() -> AnyPublisher<UIImage?, Error> {
        guard let url = getURL() else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() }
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(handleResponse)
            .mapError{$0}
            .eraseToAnyPublisher()
    }
    
    func downloadWithAsync() async throws -> UIImage? {
        // 1. weak self -> no need
        // 2. safer code (completionHandler usage X)
        guard let url = getURL() else { throw URLError(.badURL) }
        do {
            let (data, response) = try await URLSession.shared.data(from: url)
            return handleResponse(data: data, response: response)
        } catch {
            throw error
        }
    }
}
handleResponse를 통해 (Data?, URLResponse?)를 핸들링하여 UIImage?를 리턴한다.Combine을 쓸 때 eraseToAnyPublisher 메소드를 통해 손쉽게 AnyPublisher<Success, Failure> 타입을 리턴, 이후 뷰 모델에서 해당 퍼블리셔를 구독 가능Async를 쓸 때 try await를 통해 실패 가능한 접근 방법을 택해야 함 → do catch를 통해 에러 핸들링 가능 → 더 이상 Result<Success, Failure>를 통해 completion을 switch case하지 않아도 됨(리턴 타입의 옵셔널도 없어지는 것은 당연한 귀결)import SwiftUI
import Combine
class DownloadImageAsyncBootCampDataService {
    let urlString: String
    init(urlString: String) {
        self.urlString = urlString
    }
    
    func getURL() -> URL? {
        guard let url = URL(string: urlString) else { return nil }
        return url
    }
    
    func downloadWithEscaping(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> ()) {
        guard let url = getURL() else { return }
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard
                let data = data,
                let image = UIImage(data: data),
                let response = response as? HTTPURLResponse,
                response.statusCode >= 200 && response.statusCode < 300 else {
                completionHandler(nil, error)
                return
            }
            completionHandler(image, nil)
        }
        .resume()
    }
    
    func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
        guard
            let data = data,
            let image = UIImage(data: data),
            let response = response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else {
            return nil
        }
        return image
    }
    
    func downloadWithEscaping2(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> ()) {
        guard let url = getURL() else { return }
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let self = self else { return }
            let image = self.handleResponse(data: data, response: response)
            completionHandler(image, error)
        }
        .resume()
    }
    
    func downloadWithCombine() -> AnyPublisher<UIImage?, Error> {
        guard let url = getURL() else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() }
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(handleResponse)
            .mapError{$0}
            .eraseToAnyPublisher()
    }
    
    func downloadWithAsync() async throws -> UIImage? {
        // 1. weak self -> no need
        // 2. safer code (completionHandler usage X)
        guard let url = getURL() else { throw URLError(.badURL) }
        do {
            let (data, response) = try await URLSession.shared.data(from: url)
            return handleResponse(data: data, response: response)
        } catch {
            throw error
        }
    }
}
class DownloadImageAsyncBootCampViewModel: ObservableObject {
    @Published var image: UIImage? = nil
    let dataService = DownloadImageAsyncBootCampDataService(urlString: "https://picsum.photos/200")
    var cancellables = Set<AnyCancellable>()
    
    init() {
        fetchImageWithAsyncTask()
    }
    
    func fetchImageWithEscapingClosure() {
        dataService.downloadWithEscaping2 { image, error in
            if let error = error {
                print(error.localizedDescription)
                return
            } else {
                DispatchQueue.main.async { [weak self] in
                    guard let self = self else { return }
                    self.image = image
                }
            }
        }
    }
    
    func fetchImageWithCombine() {
        dataService.downloadWithCombine()
            .sink { completion in
                switch completion {
                case .failure(let error):
                    print(error.localizedDescription)
                    break
                case .finished:
                    break
                }
            } receiveValue: { [weak self] image in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    self.image = image
                }
            }
            .store(in: &cancellables)
    }
    
    func fetchImageWithCombine2() {
        dataService.downloadWithCombine()
            .receive(on: DispatchQueue.main)
            .sink { completion in
                switch completion {
                case .failure(let error):
                    print(error.localizedDescription)
                    break
                case .finished:
                    break
                }
            } receiveValue: { [weak self] image in
                guard let self = self else { return }
                self.image = image
            }
            .store(in: &cancellables)
    }
    
    func fetchImageWithAsyncTask() {
        Task {
            await fetchImageWithAsync()
        }
    }
    
    func fetchImageWithAsync() async {
        do {
            guard let image = try await dataService.downloadWithAsync() else {
                return
            }
            await MainActor.run {
                self.image = image
            }
        } catch {
            print(error.localizedDescription)
        }
    }
}
weak self를 통해 강한 참조 사이클을 피하는 데 주의AnyCancellables 집합 변수에 해당 값을 저장하는 데 주의async로 데이터 서비스가 리턴하는 데이터를 받아올 때 에러를 throw할 수 있기 때문에 do catch가 필수이고, await해야 함 → image를 UI 업데이트할 때 MainActor 클래스 내부에서 업데이트 헤야 함 → 해당 async 작업은 Task를 통해 일어나야 함struct DownloadImageAsyncBootCamp: View {
    @StateObject private var viewModel = DownloadImageAsyncBootCampViewModel()
    
    var body: some View {
        VStack {
            if let image = viewModel.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 250, height: 250, alignment: .center)
                    .onTapGesture {
                        viewModel.fetchImageWithAsyncTask()
                    }
            }
        }
    }
}
