class DownloadImageAsyncViewModel: ObservableObject {
@Published var image: UIImage? = nil
func fetchImage() {
self.image = UIImage(systemName: "heart.fill")
}
}
struct DownloadImageAsync: View {
@StateObject private var viewModel = DownloadImageAsyncViewModel()
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 250, height: 250)
}
}
.onAppear {
viewModel.fetchImage()
}
}
}
요런 뷰가 있다고 해보자
보통 viewModel에서 직접 fetch하지는 않을거니까
Image불러와주는 객체도 하나 만들어줍시다
URLSession으로 data를 처리하는 방법은 여러가지가 있을텐데
우선은 escaping으로 먼저 하는 방법을 살펴봅시다
class DownloadImageAsyncImageLoader {
let url = URL(string: "https://picsum.photos/200")!
func downloadWithEscaping(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> ()) {
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 >= 2000 && response.statusCode < 300 else {
completionHandler(nil, error)
return
}
completionHandler(image, nil)
}
.resume()
}
}
예전에 많이쓰던거!
URLSession이 끝나고나면 escaping으로 선언한 completionHandler로
받아온 결과값들을 처리해주면 되겠죠
class DownloadImageAsyncViewModel: ObservableObject {
@Published var image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
func fetchImage() {
loader.downloadWithEscaping { [weak self] image, error in
if let image = image {
self?.image = image
}
}
}
}
뷰모델도 요렇게 바뀌겠죠~!
download 메소드를 호출하면 closure에서 가지고 있는 image를 viewModel의 image에 넣어주면 끝!
근데 이걸 시뮬레이터에서 실행하면 보라색 에러가 뜹니다!!
URL처리가 백그라운드 스레드에서 동작하고 있었는데
UI를 업데이트 하는 스레드에 바로 집어 넣으니까 하면 안된다고 얘기해주는겨!
DispatchQueue로 main스레드에 다시 붙여주면 되겠죠!!
으아아앙아아
너무좋다
이제야 코드가 보이기 시작함!!!
class DownloadImageAsyncImageLoader {
let url = URL(string: "https://picsum.photos/200")!
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 downloadWithEscaping(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> ()) {
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
let image = self?.handleResponse(data: data, response: response)
completionHandler(image, error)
}
.resume()
}
}
URLSession에 있던 부분 handleResponse로 따로 빼주고
weak self 붙여줌
이번에는 combine으로 다운로드 해봅시다
URLSession으로 처리하고, Publisher가 되게해줌!
뷰모델에서 나머지 처리해주고
뿜!
combine으로 다운로드 이미지 완료!
여기서 DispatchQueue를 하는 건 combine way가 아니죠~
여기서 잠깐!
DisPatchQueue.main vs RunLoop.main 차이
RunLoop.main은 Foundation 프레임워크에서 제공하는 더 낮은 수준의 API입니다. 애플리케이션의 기본 실행 루프에 대한 객체 지향적 인터페이스를 제공합니다. RunLoop.main을 사용하면 코드를 특정 시간에 메인 스레드에서 실행하도록 예약하거나, 소스 및 타이머를 설정하여 실행 흐름을 제어할 수 있습니다.
Combine에서 DispatchQueue.main과 RunLoop.main 모두 Publisher가 값을 발생시킨 후 메인 스레드에서 값을 받기 위해 사용될 수 있습니다. 두 방법의 차이점은 DispatchQueue.main이 더 추상적인 높은 수준의 API이며, RunLoop.main이 더 낮은 수준의 API이며 실행 흐름을 더 세밀하게 제어할 수 있다는 것입니다.
대부분의 경우에는 DispatchQueue.main을 사용하는 것이 권장되며, 대부분의 iOS 개발 시나리오에서 가장 간단하고 읽기 쉬운 방법입니다. RunLoop.main을 사용하는 것은 보다 복잡하고 읽기 어려울 수 있으며, 실행 흐름을 더 세밀하게 제어해야 할 때 사용됩니다.
이제 세번째 방법인 async 를 사용해봅시다!!
URLSession.shared.data로 initialize가능한 애들 보다보면 gray색으로 지금 당장은 안되는 구문이 있음!
이거는 간단하게 함수 선언부에 async키워드 작성해주면 됩니다~
func downloadWithAsync() async throws -> UIImage? {
do {
let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)
return handleResponse(data: data, response: response)
} catch {
throw error
}
}
요런 느낌!!!
async라는 키워드를 붙여주고 await을 붙여주면 sync코드처럼 순서대로 진행됨!!
말그대로 기다린다고
뷰모델로 와서 방금 만든 메소드를 쓰려고하면 마찬가지로 concurrency를 지원하지 않는 context에 있다고 뜸
async붙여주고!
error처리 따로 안하고 싶을 땐 지금처럼 try? 붙여주면 된다고 했죠
그리고 await키워드!! 결과값이 도착할때까지 기다린다고 해주는겨!!!
그럼 image가 도착하면 self.image=image
가 실행이 되겠죠?
onAppear에서 이 메소드를 호출할때는 concurrency를 지원하는 구문안에 있어야함! Task가 그중에 하나고 마찬가지로 await키워드 붙여줘야겠죠
시뮬레이터 실행하면 마찬가지로 보라에러 나오는데
DispatchQueue를 쓰면 안됨!!
Actor를 써야함!!
요렇게! MainActor.run구문 안에서 UI를 업데이트하는 로직이 실행되야합니다~
class머리에 @MainActor로 붙이는 게 요런느낌이겠구나!!
코드가 깔끔해짐!
비동기 처리를 하는 코드구나! 바로 알 수 있음
[weak self] 안붙여줘도됨!