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()
}
}
}
}
}