iOS/SwiftUI
[SwiftUI] Async/Await, @escaping, and Combine
- -
[참고영상]
[학습 목표]
Async/ Await을 이용한 방법과 Combine을 이용한 방법 그리고 @escaping 을 이용해 인터넷에서 비동기 처리를 통해 image 데이터를 받아오자.
[구현 방법]
<공통 부분>
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
}
|
cs |
해당 함수를 통해서 Image를 리턴해오자.
1. @escaping을 이용한 방법
1
2
3
4
5
6
7
|
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()
}
|
cs |
해당 함수에서 completionHandler를 이용해서 error가 뜨는지 확인을 해 준다.
해당 내용에서 @escaping은 클로저의 종류 중 하나로 함수의 동작 순서를 보면
1. 클로저가 downloadWithEscaping 함수의 completionHandler 인자로 전달됨.
2. 클로저 completionHandler는 마지막에 함수가 종료 된 이후에 실행이 됨.
즉, 함수 밖에서 탈출을 한 뒤에 실행이 되기 때문에 Escaping 클로저라는 이름이 붙는다.
해당 함수에서 escaping을 쓴 이유는 URLSession을 이용해 url 요청이 끝난 후 비동기로 실행이 되어야 하기 때문이다.
2. Combine을 활용한 방법
1
2
3
4
5
6
7
8
|
import Combine
func downloadWithCombine() -> AnyPublisher<UIImage?, Error>{
URLSession.shared.dataTaskPublisher(for: url)
.map(handleResponse)
.mapError({ $0 })
.eraseToAnyPublisher()
}
|
cs |
Combine을 사용하기 위해서는 Combine framework를 사용해야 한다.
3. Async/Await를 사용한 방법
1
2
3
4
5
6
7
8
|
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
}
}
|
cs |
해당 방법은 위의 2가지 방법보다 해당 프로젝트에서는 조금 더 효율적이다. (해당 영상에서 말하였지만 자세한 이유는 나중에 따로 찾아봐야 할듯.)
이전 시간에 배웠던 throws를 이용해 error 구문을 잡을 수가 있다.
해당 문법에서 await는 1분을 기다리거나 1시간을 기다리는 것이 아니라 해당 url 에서 반응이 돌아올때까지 계속해서 기다린다.
[예시]

[코드]
<Escaping를 이용한 구현>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
import SwiftUI
import Combine
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()
}
}
class DownloadImageAsyncViewModel: ObservableObject{
@Published var image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
var cancellables = Set<AnyCancellable>()
func fetchImage() async {
loader.downloadWithEscaping { [weak self] image, error in
if let image = image {
self?.image = image
}
}
}
}
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{
Task {
await viewModel.fetchImage()
}
}
}
}
struct DownloadImageAsync_Previews: PreviewProvider {
static var previews: some View {
DownloadImageAsync()
}
}
|
cs |
<Combine을 이용한 구현>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
import SwiftUI
import Combine
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 downloadWithCombine() -> AnyPublisher<UIImage?, Error>{
URLSession.shared.dataTaskPublisher(for: url)
.map(handleResponse)
.mapError({ $0 })
.eraseToAnyPublisher()
}
}
class DownloadImageAsyncViewModel: ObservableObject{
@Published var image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
var cancellables = Set<AnyCancellable>()
func fetchImage() async {
loader.downloadWithCombine()
.receive(on: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] image in
self?.image = image
}
.store(in: &cancellables)
}
}
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{
Task {
await viewModel.fetchImage()
}
}
}
}
struct DownloadImageAsync_Previews: PreviewProvider {
static var previews: some View {
DownloadImageAsync()
}
}
|
cs |
<Async / Await 이용한 구현>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
import SwiftUI
import Combine
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 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
}
}
}
class DownloadImageAsyncViewModel: ObservableObject{
@Published var image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
var cancellables = Set<AnyCancellable>()
func fetchImage() async {
let image = try? await loader.downloadWithAsync()
await MainActor.run {
self.image = image
}
}
}
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{
Task {
await viewModel.fetchImage()
}
}
}
}
struct DownloadImageAsync_Previews: PreviewProvider {
static var previews: some View {
DownloadImageAsync()
}
}
|
cs |
Contents
소중한 공감 감사합니다