Beginning Networking with URLSession

Sep 13 2022 · Swift 5.6, iOS 15, Xcode 13.4.1

Part 2: Download Data

17. Pause, Resume & Cancel Downloads

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 16. Simulate Different Network Speeds Next episode: 18. Conclusion

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Notes: 17. Pause, Resume & Cancel Downloads

URLSessionConfiguration - Apple Developer URLSession - Apple Developer

Heads up... You've reached locked video content where the transcript will be shown as obfuscated text.

When users start to download a file, they may later change their mind about the download. The file may be too large or their connection is too slow. Or maybe, they need to pause the download so they can download another file first.

import SwiftUI
class MutableSongDownloader: NSObject, ObservableObject {
  @Published var downloadLocation: URL?
}
private lazy var session: URLSession = {
  let configuration = URLSessionConfiguration.default
      
  return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
extension MutableSongDownloader: URLSessionDownloadDelegate {
  func urlSession(_ session: URLSession,
                  downloadTask: URLSessionDownloadTask,
                  didWriteData bytesWritten: Int64, 
                  totalBytesWritten: Int64,
                  totalBytesExpectedToWrite: Int64) {

  }
  
  func urlSession(_ session: URLSession,
                  downloadTask: URLSessionDownloadTask,
                  didFinishDownloadingTo location: URL) {
    
  }
  
  func urlSession(_ session: URLSession,
                  task: URLSessionTask,
                  didCompleteWithError error: Error?) {
    
  }
}
private var downloadURL: URL?
private var downloadTask: URLSessionDownloadTask?
func downloadSong(at url: URL) {
  downloadURL = url
    
  downloadTask = session.downloadTask(with: url)
  downloadTask?.resume()
}
@Published var downloadProgress: Float = 0
func urlSession(_ session: URLSession,
                downloadTask: URLSessionDownloadTask,
                didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
                totalBytesExpectedToWrite: Int64) {
  Task {
    await MainActor.run {
      downloadProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
    }
  }
}       
func urlSession(_ session: URLSession,
                task: URLSessionTask,
                didCompleteWithError error: Error?
  ) {
  Task {
    await MainActor.run {
      if let httpResponse = task.response as? HTTPURLResponse,
         httpResponse.statusCode != 200 {
          
        print("Request failed.")
      }
    }
  }
}
func urlSession(_ session: URLSession,
                downloadTask: URLSessionDownloadTask,
                didFinishDownloadingTo location: URL
  ) {
  let fileManager = FileManager.default
    
  guard let documentsPath = fileManager.urls(for: .documentDirectory,
                                             in: .userDomainMask).first,
        let lastPathComponent = downloadURL?.lastPathComponent
  else {
  print("Document directory error.")
      
  return
  }
    
  let destinationURL = documentsPath.appendingPathComponent(lastPathComponent)
    
  do {
    if fileManager.fileExists(atPath: destinationURL.path) {
      try fileManager.removeItem(at: destinationURL)
    }
        
    try fileManager.copyItem(at: location, to: destinationURL)
      
    Task {
      await MainActor.run {
        downloadLocation = destinationURL
      }
    }
  } catch {
    Task {
      await MainActor.run {
        print("Error copying song.")
      }
    }
  }
}

Time to Support Cancel, Pause, And Resume

Because MutableSongDownloader can pause, resume, or cancel, you want a way to keep track of the state of your download. To do so, add the following enum inside of your class:

enum State {
  case paused
  case downloading
  case failed
  case finished
  case waiting
}
var state: State = .waiting
state = .downloading
func cancel() {
  state = .waiting
   
 downloadTask?.cancel()
    
  Task {
    await MainActor.run {
      downloadProgress = 0
    }
  }
}
private var resumeData: Data?
func pause() {
  downloadTask?.cancel(byProducingResumeData: { data in
    Task {
      await MainActor.run {
        self.resumeData = data
          
        self.state = .paused
      }
    }
  })
}
func resume() {
 guard let resumeData = resumeData else {
   return
 }
   
 downloadTask = session.downloadTask(withResumeData: resumeData)
 downloadTask?.resume()
   
 state = .downloading
}
Task {
  await MainActor.run {
    state = .failed
  }
} 
state = .finished
state = .failed
state = .failed
@ObservedObject private var mutableDownloader: MutableSongDownloader = MutableSongDownloader()
private func mutableDownloadTapped() {
  switch mutableDownloader.state {
  case .downloading:
    mutableDownloader.pause()
      
  case .failed, .waiting:
    guard let previewURL = musicItem.previewURL else {
      return
    }
      
    mutableDownloader.downloadSong(at: previewURL)

  case .finished:
    playMusic = true
      
  case .paused:
    mutableDownloader.resume()
  }
}
AudioPlayer(songUrl: mutableDownloader.downloadLocation!)
if mutableDownloader.state == .paused || mutableDownloader.state == .downloading {
  ProgressView(value: mutableDownloader.downloadProgress)
}
Button<Text>(action: mutableDownloadTapped) {
  switch mutableDownloader.state {
  case .downloading:
    return Text("Pause")
                
  case .failed:
    return Text("Retry")
                
  case .finished:
    return Text("Listen")
                
  case .paused:
    return Text("Resume")
                
  case .waiting:
    return Text("Download")
  }
}