在SwiftUI 如何在URLSessionDownloadTask中實現Pause、Resume、Cancel等功能
xcode 13.4.1, swift 5.5, iOS 15.4
2022-09-12
通常在下載一些大檔案時,需要的時間會很長,或網絡不好時先暫停下載,能網絡好了,再繼續進行下載,避免每次都要重新下載,這次我們來講講在如何在URLSessionDownloadTask中實現Pause、Resume、Cancel等功能。
1. 首先講講下載的幾個狀態的變化,首先在還沒有開始下載之前的等待(waiting),在使用者按完下載(Download),狀態變成下載中(downloading),在下載中可以按下暫定(Pause),狀態就會變成暫定中(Pause),若按下取消鍵(Cancel),下載狀態又回到等待狀態(waiting),在暫定狀態中,使用者可以有取消(Cancel)和繼續下載(Resume),狀態流程圖如下:
2. 以下分別先講講如何實現URLSessionDownloadTask中實現Download、Pause、Resume、Cancel三種功能:
3. 實現URLSessionDownloadTask的下載Download功能:
func fetchFileAtUrl(_ url : URL) {
self.fileUrl = url
let configuration = URLSessionConfiguration.default
self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
guard let urlSession = self.urlSession else {
return
}
self.downloadTask = urlSession.downloadTask(with: fileUrl!)
self.downloadTask?.resume()
DispatchQueue.main.async {
self.state = .downloading
self.downloadPercent = 0.0
}
}
4. 實現URLSessionDownloadTask的取消Cancel功能:
func cancel() {
state = .waiting
self.downloadTask?.cancel()
DispatchQueue.main.async {
self.downloadPercent = 0.0
}
}
5. 實現URLSessionDownloadTask的暫停Pause功能,在暫停時,需要將已下載的數據保存起來,等Resume時再傳給下載任務:
func pause() {
downloadTask?.cancel(byProducingResumeData: { data in
DispatchQueue.main.async {
self.resumeData = data
self.state = .paused
}
})
}
6. 實現URLSessionDownloadTask的繼續Resume功能,繼續下載時,要把之前已下載的數據傳回給下載任務:
func resume() {
guard let resumeData = resumeData else {
return
}
guard let urlSession = self.urlSession else {
return
}
downloadTask = urlSession.downloadTask(withResumeData: resumeData)
downloadTask?.resume()
DispatchQueue.main.async {
self.state = .downloading
}
}
import SwiftUI
class Downloader : NSObject, ObservableObject, URLSessionDownloadDelegate {
var fileUrl : URL?
var downloadTask : URLSessionDownloadTask?
var urlSession : URLSession?
var resumeData : Data?
@Published var fileLocation : URL?
@Published var downloadPercent : Float = 0
@Published var state : DownloadState = .waiting
enum DownloadState {
case waiting
case downloading
case paused
case finished
}
func fetchFileAtUrl(_ url : URL) {
self.fileUrl = url
let configuration = URLSessionConfiguration.default
self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
guard let urlSession = self.urlSession else {
return
}
self.downloadTask = urlSession.downloadTask(with: fileUrl!)
self.downloadTask?.resume()
DispatchQueue.main.async {
self.state = .downloading
self.downloadPercent = 0.0
}
}
func cancel() {
state = .waiting
self.downloadTask?.cancel()
DispatchQueue.main.async {
self.downloadPercent = 0.0
}
}
func pause() {
downloadTask?.cancel(byProducingResumeData: { data in
DispatchQueue.main.async {
self.resumeData = data
self.state = .paused
}
})
}
func resume() {
guard let resumeData = resumeData else {
return
}
guard let urlSession = self.urlSession else {
return
}
downloadTask = urlSession.downloadTask(withResumeData: resumeData)
downloadTask?.resume()
DispatchQueue.main.async {
self.state = .downloading
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError()
}
guard let lastPathComponent = self.fileUrl?.lastPathComponent else {
fatalError()
}
let fileLocation = documentPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: fileLocation.path) {
try fileManager.removeItem(at: fileLocation)
}
try fileManager.copyItem(at: location, to: fileLocation)
DispatchQueue.main.async {
self.state = .finished
self.downloadPercent = 100.0
self.fileLocation = fileLocation
}
} catch {
print(error)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
DispatchQueue.main.async {
self.downloadPercent = Float(totalBytesWritten*100)/Float(totalBytesExpectedToWrite)
}
}
}
struct DownloadExample: View {
@State var playFlag = false
@ObservedObject var downloader = Downloader()
var body: some View {
VStack {
if downloader.state == .waiting {
Button {
guard let fileUrl = URL(string : "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/0c/94/76/0c9476bc-7ef0-c690-de28-c441a35c11e5/mzaf_11460274548565212191.plus.aac.p.m4a") else {
return
}
downloader.fetchFileAtUrl(fileUrl)
} label: {
Text("Download")
}
} else if downloader.state == .downloading {
Button {
downloader.cancel()
} label: {
Text("Cancel")
}
Button {
downloader.pause()
} label: {
Text("Pause")
}
Text("\(downloader.downloadPercent)% downloaded")
} else if downloader.state == .paused {
Button {
downloader.cancel()
} label: {
Text("Cancel")
}
Button {
downloader.resume()
} label: {
Text("Resume")
}
Text("\(downloader.downloadPercent)% downloaded")
} else if downloader.state == .finished {
Button {
guard let fileUrl = URL(string : "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/0c/94/76/0c9476bc-7ef0-c690-de28-c441a35c11e5/mzaf_11460274548565212191.plus.aac.p.m4a") else {
return
}
downloader.fetchFileAtUrl(fileUrl)
} label: {
Text("Redownload")
}
Button {
playFlag.toggle()
} label: {
Text("Play")
}
Text("Finished")
}
}.sheet(isPresented: $playFlag) {
AudioPlayerView(url : downloader.fileLocation!)
}
}
}