[SwiftUI] Continuations

Junyoung Park·2022년 8월 28일
0

SwiftUI

목록 보기
60/136
post-thumbnail

How to use Continuations in Swift (withCheckedThrowingContinuation) | Swift Concurrency #7

Continuations

구현 목표

  • 현재 태스크를 지연, 주어진 클로저 내에 확인한(안전한) continuation을 리턴
  • 이스케이핑 클로저를 사용하는 기존 API를 async await 함수로 핸들링하기
  • continuation.resume을 통한 데이터 리턴 및 에러 던지기

구현 태스크

  1. 컴플리션 핸들러의 결과에 따라 서로 다른 Continuation 리턴하기
  2. 커스텀 컴플리션 핸들러에 따른 Continuation 리턴 함수 구현
  3. UI 데이터 패치를 위한 MainActor 바인딩

핵심 코드

    func fetchData2(url: URL) async throws -> Data {
        // Convert API's completion handler to async version
        return try await withCheckedThrowingContinuation { continuation in
            URLSession.shared.dataTask(with: url) { data, response, error in
                if let data = data {
                    continuation.resume(returning: data)
                } else if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(throwing: URLError(.badURL))
                }
                // A closure that takes an UnsafeContinuation parameter. You must resume the continuation exactly once.
            }
            .resume()
        }
    }

소스 코드

import SwiftUI

protocol CheckedContinuationBootCampProtocol {
    func fetchData(url: URL) async throws -> Data
    func fetchData2(url: URL) async throws -> Data
    func fetchDataFromDatabase() async throws -> UIImage
}

class CheckedContinuationBootCampDataService: CheckedContinuationBootCampProtocol {
    func fetchData(url: URL) async throws -> Data {
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            return data
        } catch {
            print(error.localizedDescription)
            throw error
        }
    }
    
    func fetchData2(url: URL) async throws -> Data {
        // Convert API's completion handler to async version
        return try await withCheckedThrowingContinuation { continuation in
            URLSession.shared.dataTask(with: url) { data, response, error in
                if let data = data {
                    continuation.resume(returning: data)
                } else if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(throwing: URLError(.badURL))
                }
                // A closure that takes an UnsafeContinuation parameter. You must resume the continuation exactly once.
            }
            .resume()
        }
    }
    
    private func fetchDataFromDatabase(completionHandler: @escaping (_ image: UIImage) -> ()) {
        // Mocking Data Fetching from DB
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            completionHandler(UIImage(systemName: "flame.fill")!)
        }
    }
    
    func fetchDataFromDatabase() async throws -> UIImage {
        return await withCheckedContinuation { continuation in
            fetchDataFromDatabase { image in
                continuation.resume(returning: image)
            }
        }
    }
}
  • continuation: 기존 컴플리션 핸들러의 성공/실패에 따라 continuation.resume 리턴 값이 변경 → 이스케이핑 클로저를 사용하는 기존 함수를 async 구문을 따르는 새로운 continuation으로 연결 가능 → 데이터 패치 코드에서 해당 continuation을 통해 async await로 패치 가능
class CheckedContinuationBootCampViewModel: ObservableObject {
    @Published var image: UIImage? = nil
    let dataService: CheckedContinuationBootCampProtocol
    let urlString: String
    let url: URL
    
    init(dataService: CheckedContinuationBootCampProtocol, urlString: String) {
        self.dataService = dataService
        self.urlString = urlString
        if let url = URL(string: urlString) {
            self.url = url
        } else {
            self.url = URL(string: "https://picsum.photos/1000")!
        }
    }
    
    func fetchImage() async {
        do {
            let data = try await dataService.fetchData(url: url)
            if let image = UIImage(data: data) {
                await MainActor.run(body: {
                    self.image = image
                })
            }
        } catch {
            print(error.localizedDescription)
        }
    }
    
    func fetchImage2() async {
        do {
            let data = try await dataService.fetchData2(url: url)
            if let image = UIImage(data: data) {
                await MainActor.run(body: {
                    self.image = image
                })
            }
        } catch {
            print(error.localizedDescription)
        }
    }
    
    func fetchImageFromDatabase() async {
        do {
            let image = try await dataService.fetchDataFromDatabase()
            await MainActor.run(body: {
                self.image = image
            })
        } catch {
            print(error.localizedDescription)
        }
    }
}
  • UI 리렌더링 시 사용할 데이터는 MainActor로 실행하기
struct CheckedContinuationsBootCamp: View {
    @StateObject private var viewModel: CheckedContinuationBootCampViewModel
    
    init(dataService: CheckedContinuationBootCampProtocol, urlString: String) {
        _viewModel = StateObject(wrappedValue: CheckedContinuationBootCampViewModel(dataService: dataService, urlString: urlString))
    }
    
    var body: some View {
        ZStack {
            if let image = viewModel.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200, height: 200)
            }
        }
        .task {
            await viewModel.fetchImage2()
        }
    }
}
  • 데이터 서비스 의존성 주입

구현 화면

profile
JUST DO IT

0개의 댓글