🍏/Swift

[Swift] Continuations > completion or delegate 를 async 로

eungding 2023. 8. 2. 14:17
728x90
반응형

과거에 Continuations 를 이용해서 짰던 코드를 보는데 헷갈렸다.. @)@

리마인드가 필요하군 

 

우선 요 목차(?)가 머릿 속에 있어야한다. 

 

https://developer.apple.com/documentation/swift/checkedcontinuation

 

 

[1] 용어 기억 

가끔 용어도 잘 생각이 안날 때가 있다,, 

 

 

[ Continuation ]

 

비동기 코드를 래핑해서 연속(continuation)을 만든다!  라고 기억하자 

문서에서는 이렇게 표현한다. 

To create a continuation in asynchronous code, 
call the withUnsafeContinuation(function:_:) or withUnsafeThrowingContinuation(function:_:) function. 

 

 

 

[ CheckedContinuation vs  UnsafeContinuation ]

 

잘 까먹는 주요원인이다.

checked / unchecked 이던가..  safe / unsafe 이던가.. 하면 더 쉽게 기억할텐데 따흑,,

 

 

CheckedContinuation 은 resume이 빠졌거나 여러번 불리는 지를 런타임때 체크하고 

UnsafeContinuation 은 체크안한다 (오버헤드를 피하기 위해)

CheckedContinuation performs runtime checks for missing or multiple resume operations. 
UnsafeContinuation avoids enforcing these invariants at runtime because it aims to be a low-overhead mechanism for interfacing Swift tasks with event loops, delegate methods, callbacks, and other non-async scheduling mechanisms

 

이 글에서 자세한 실험을 볼 수 있다. 

 

 

[2] 사용예시

 

iOS 비동기프로그래밍의 두가지 주요 패턴인

callback (또는 completion hanlder), delegate 를 async-await syntax 로 쓰고 싶을 때 사용한다.

 

 

1) completion -> async/await 

 

completion handler 를 파라미터로 받는 function을  래핑해서 async function 을 만들고 싶을 때 사용한다.

 

여기 예제 잘 설명되어있음 

http://minsone.github.io/swift-concurrency-continuation

 

[Swift 5.7+][Concurrency] Continuations - Closure를 async 코드로 감싸 사용하기

Continuation은 프로그램 상태의 불투명한 표현입니다. 비동기 코드에서 연속(continuation)을 만들려면 withCheckedContinuation(function:_:), withCheckedThrowingContinuation(function:_:) 와 같은 코드를 호출합니다. 비동

minsone.github.io

 

 

2) delegate -> async/await 

 

delegate 패턴 대신 async function 을 만들고 싶을때 사용한다. 

 

예를들어

StoreKit 1 의 대부분은 delegate 패턴인데 

ProductFetcher, ReceiptLoader 등 각각 객체를 만들고 async/await 를 사용할 수 있게 해주면 코드가 훨씬 간결해진다.

 

fetchAvailableProducts 을 async  function으로 만드려면

request 가 성공, 실패할때 불리는 delegate function 내에서 resume이 호출되게 해주면 된다. 

class IAPProductsFetcher: NSObject, SKProductsRequestDelegate {

    private var productsRequest: SKProductsRequest?
    private var productsRequestDoneAction: (([SKProduct]) -> Void)?
    
    private let productIds: [String]

    init(productIds: [String] = IAPProducts.identifiers) {
        self.productIds = productIds
    }
    
    func fetchAvailableProducts() async -> [SKProduct] {
        return await withCheckedContinuation { continuation in
            self.productsRequestDoneAction = {
                continuation.resume(returning: $0)
            }
            self.fetchProducts(matchingIdentifiers: self.productIds)
        }
    }
    
    private func fetchProducts(matchingIdentifiers identifiers: [String]) {
        let productIdentifiers = Set(identifiers)
        
        let productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productsRequest.delegate = self
        self.productsRequest = productsRequest
        
        productsRequest.start()
    }
    
    // MARK: - SKProductsRequestDelegate
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        let availableProducts = response.products
        self.productsRequestDoneAction?(availableProducts)
        self.productsRequestDoneAction = nil
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        self.productsRequestDoneAction?([])
        self.productsRequestDoneAction = nil
    }
}

 

 

혹은 Modern Cuncurrency 책 에 나오는 예제처럼 Continuation 자체를 프로퍼티로 들고 있어도 좋다.

class ChatLocationDelegate: NSObject, CLLocationManagerDelegate {

  typealias LocationContinuation = CheckedContinuation<CLLocation, Error>
  private var continuation: LocationContinuation?

  ... 
}

 

full code 도 함께 첨부  (출처: Modern Concurrency In Swift > chapter 5)

import Foundation
import CoreLocation

class ChatLocationDelegate: NSObject, CLLocationManagerDelegate {
  typealias LocationContinuation = CheckedContinuation<CLLocation, Error>
  private var continuation: LocationContinuation?

  init(manager: CLLocationManager, continuation: LocationContinuation) {
    self.continuation = continuation
    super.init()
    manager.delegate = self
    manager.requestWhenInUseAuthorization()
  }

  deinit {
    continuation?.resume(throwing: CancellationError())
  }

  func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    switch manager.authorizationStatus {
    case .notDetermined:
      break
    case .authorizedAlways, .authorizedWhenInUse:
      manager.startUpdatingLocation()
    default:
      continuation?.resume(
        throwing: "The app isn't authorized to use location data"
      )
      continuation = nil
    }
  }

  func locationManager(
    _ manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]
  ) {
    guard let location = locations.first else { return }
    continuation?.resume(returning: location)
    continuation = nil
  }

  func locationManager(
    _ manager: CLLocationManager,
    didFailWithError error: Error
  ) {
    continuation?.resume(throwing: error)
    continuation = nil
  }
}
class BlabberModel: ObservableObject {
  var username = ""
  var urlSession = URLSession.shared
  private let manager = CLLocationManager()
  private var delegate: ChatLocationDelegate?

  /// Shares the current user's address in chat.
  func shareLocation() async throws {
    let location: CLLocation = try await
    withCheckedThrowingContinuation { [weak self] continuation in
      self?.delegate = ChatLocationDelegate(manager: manager, continuation: continuation)
      if manager.authorizationStatus == .authorizedWhenInUse {
        manager.startUpdatingLocation()
      }
    }
   }
    
    ...
 }

 

 

[3] 참고 > continuation

 

Modern Concurrency 책에 continuation 에 대한 자세한 설명이 나온다.

이걸 읽으면 continuation 를 더 잘 이해할 수 있음!

 


A continuation is an object that tracks a program’s state at a given point.

The Swift concurrency model assigns each asynchronous unit of work a continuation instead of creating an entire thread for it. This allows the concurrency model to scale your work more effectively based on the capabilities of the hardware. It creates only as many threads as there are available CPU cores, and it switches between continuations instead of between threads, making it more efficient.

You’re familiar with how an await call works: Your current code suspends execution and hands the thread and system resources over to the central handler, which decides what to do next.

When the awaited function completes, your original code resumes, as long as no higher priority tasks are pending. But how?

When the original code suspends, it creates a continuation that represents the entire captured state at the point of suspension. When it’s time to resume execution or throw, the concurrency system recreates the state from the continuation and the work… well, continues.

 

 

This all happens behind the scenes when you use async functions. You can also create continuations yourself, which you can use to extend existing code that uses callbacks or delegates.

These APIs can benefit from using await as well.

Manually creating continuations allows you to migrate your existing code gradually to the new concurrency model.

 

반응형