Help me converting completionHandler func to async/await

So I have a class like below

class DownloadOperation: Operation {
  
  private var task : URLSessionDownloadTask!
  
  enum OperationState : Int {
    case ready
    case executing
    case finished
  }
  
  // default state is ready (when the operation is created)
  private var state : OperationState = .ready {
    willSet {
      self.willChangeValue(forKey: "isExecuting")
      self.willChangeValue(forKey: "isFinished")
    }
    
    didSet {
      self.didChangeValue(forKey: "isExecuting")
      self.didChangeValue(forKey: "isFinished")
    }
  }
  
  override var isReady: Bool { return state == .ready }
  override var isExecuting: Bool { return state == .executing }
  override var isFinished: Bool { return state == .finished }
  
  init(
    session: URLSession,
    downloadTaskURL: URL,
    item: ItemModel,
    completionHandler: ((URL?, URLResponse?, ItemModel, Error?) -> Void)?
  ) {
    
    super.init()
    
    // use weak self to prevent retain cycle
    task = session.downloadTask(
      with: downloadTaskURL, completionHandler: { [weak self] (localURL, response, error) in
        
        /*
         if there is a custom completionHandler defined,
         pass the result gotten in downloadTask's completionHandler to the
         custom completionHandler
         */
        if let completionHandler = completionHandler {
          // localURL is the temporary URL the downloaded file is located
          completionHandler(localURL, response, item, error)
        }
        
        /*
         set the operation state to finished once
         the download task is completed or have error
         */
        self?.state = .finished
      })
  }
  
  override func start() {
    /*
     if the operation or queue got cancelled even
     before the operation has started, set the
     operation state to finished and return
     */
    if(self.isCancelled) {
      state = .finished
      return
    }
    
    // set the state to executing
    state = .executing
    
    // start the downloading
    self.task.resume()
  }
  
  override func cancel() {
    super.cancel()
    
    // cancel the downloading
    self.task.cancel()
  }
}

I would like to call it in async func, but I'm having difficulties with converting it to asyn func

  func getItemsAsync() async {
    requestStatus = .pending
    
    do {
      let feedsData = try await dataService.fetchAllFeedsAsync()
      
      for index in feedsData.indices {
        var item = feedsData[index]
        
        // make sure Item has a URL
        guard let videoURL = item.url else { return }
        
        let operation = DownloadOperation(
          session: URLSession.shared,
          downloadTaskURL: videoURL,
          item: item,
          completionHandler: { [weak self] (localURL, response, item, error) in
            
            guard let tempUrl = localURL else { return }
            
            let saveResult = self?.fileManagerService.saveInTemp(tempUrl, fileName: videoURL.lastPathComponent)
            
            switch saveResult {
            case .success(let savedURL):
              let newItem: ItemModel = .init(
                id: item.id,
                player: AVPlayer(url: savedURL)
              )
              
              await MainActor.run(body: {
                self?.items.append(newItem)
                
                if items.count ?? 0 > 1 {
                  // once first video is downloaded, use all device cores to fetch next videos
                  // all newest iOS devices has 6 cores
                  downloadQueue.setMaxConcurrentOperationCount(.max)
                }
              })
            case .none: break
            case .failure(_):
              EventTracking().track("Video download fail", [
                "id": item.id,
                "ulr": videoURL.absoluteString.decodeURL()
              ])
            }
            
          })
        
        let fileCaheURL = downloadQueue.queueDownloadIfFileNotExists2(videoURL, operation)
        
        if let fileCaheURL = fileCaheURL {
          // ... do some other magic
        }
      }
      
    } catch let error {
      requestStatus = .error
      errorMessage = error.localizedDescription
    }
  }

Replies

Do you want to continue with this being an Operation subclass?

If so, I wouldn’t recommend switching to Swift concurrency. Operation has its own view of concurrency and, while that’s a completely valid view of the world, it doesn’t gel well with Swift concurrency. If you’re going to rewrite this, it’d be better to steer clear of Operation entirely. And if you can’t do that, because Operation is critical to other parts of your app, it’d be better to just not worry about rewriting this operation.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • @eskimo I am using Operation to download videos from the web. My plan is to use 1 operation if this is a first video (to get full mobile bandwidth) and then once it is downloaded, allow to use all (6) cores and download 6 videos at the same time. Maybe my approach is wrong? If not, how to do it with swift concurrency? I couldn't find a solution that's why I choose Operation

Add a Comment