本文翻譯自 NSURLSession Tutorial: Getting Started
App 無論是從服務(wù)器拉取應(yīng)用數(shù)據(jù)鸵赫,還是更新社交媒體狀態(tài)或是下載遠(yuǎn)程文件到硬盤里,都是 HTTP 網(wǎng)絡(luò)請(qǐng)求實(shí)現(xiàn)的屹电,它們就是移動(dòng)應(yīng)用的心臟陆盘。
為了滿足開發(fā)者對(duì)于網(wǎng)絡(luò)請(qǐng)求的眾多要求碧聪,蘋果提供了 NSURLSession株汉,這是一套完整的網(wǎng)絡(luò) API 方法筐乳,用于通過 HTTP 上傳和下載內(nèi)容。
在本教程中乔妈,我們會(huì)學(xué)習(xí)如何使用 NSURLSession 構(gòu)建 Half Tunes app蝙云,它可以讓我們查詢 iTunes Search API 并下載選中歌曲的 30 秒試聽。最終的 app 還會(huì)支持后臺(tái)傳輸路召,用戶可以暫停勃刨、恢復(fù)或取消正在進(jìn)行中的下載。
開始
下載 啟動(dòng)項(xiàng)目 股淡;它已包含用于搜索歌曲和顯示結(jié)果的用戶界面身隐,還有用于解析 JSON 和播放曲目的幫助方法。它們可以讓你專注于實(shí)現(xiàn) app 的網(wǎng)絡(luò)部分揣非。
構(gòu)建并運(yùn)行項(xiàng)目抡医;可以看到一個(gè)視圖躲因,搜索條在頂端早敬,空的表格視圖在下方:
在搜索條中輸入然后點(diǎn)擊 Search忌傻。視圖仍然是空白的,但不用擔(dān)心搞监;我們會(huì)調(diào)用新的 NSURLSession 以改變這種情況水孩。
NSURLSession 概況
開始之前,有必要了解一下 NSURLSession 以及它的組成部分琐驴,所以花一分鐘看一遍下面的快速概況俘种。
NSURLSession 技術(shù)上是既是一個(gè)類,也是一組用于處理基于 HTTP/HTTPS 請(qǐng)求的類:
NSURLSession 是負(fù)責(zé)發(fā)送和接收 HTTP 請(qǐng)求的關(guān)鍵對(duì)象绝淡。通過 NSURLSessionConfiguration 來創(chuàng)建它宙刘,有三種風(fēng)格:
- defaultSessionConfiguration:創(chuàng)建默認(rèn)配置的對(duì)象,使用硬盤持久化的全局緩存牢酵、credential 和 cookie 存儲(chǔ)對(duì)象悬包。
- ephemeralSessionConfiguration:和默認(rèn)配置類似,除了所有會(huì)話相關(guān)的數(shù)據(jù)都被存儲(chǔ)在內(nèi)容中馍乙。把它想象成“私人”會(huì)話布近。
- backgroundSessionConfiguration:允許會(huì)話在后臺(tái)執(zhí)行上傳和下載任務(wù)。即使 app 本身被暫退扛瘢或終止了撑瞧,傳輸都會(huì)繼續(xù)。
NSURLSessionConfiguration 還可以讓你配置會(huì)話屬性显蝌,如超時(shí)值预伺、緩存策略和附加 HTTP 頭。有關(guān)配置選項(xiàng)的完整列表曼尊,請(qǐng)參見 文檔 扭屁。
NSURLSessionTask 是表示任務(wù)對(duì)象的抽象類。會(huì)話會(huì)創(chuàng)建一個(gè)任務(wù)涩禀,用來執(zhí)行獲取數(shù)據(jù)和下載料滥、上傳文件的實(shí)際工作。
在這個(gè)情境中艾船,有三種類型的具體會(huì)話任務(wù):
- NSURLSessionDataTask:將此任務(wù)用于 HTTP GET 請(qǐng)求以及把服務(wù)器數(shù)據(jù)取到內(nèi)容中葵腹。
- NSURLSessionUploadTask:使用此任務(wù)可以將文件從磁盤上傳到Web服務(wù),通??常通過HTTP POST或PUT方法屿岂。
- NSURLSessionDownloadTask:使用此任務(wù)將文件從遠(yuǎn)程服務(wù)下載到臨時(shí)文件位置践宴。
我們還可以暫停、恢復(fù)和取消任務(wù)爷怀。NSURLSessionDownloadTask 具有額外的暫停功能以備未來可以恢復(fù)阻肩。
通常,NSURLSession 以兩種方式返回?cái)?shù)據(jù):當(dāng)任務(wù)成功完成或出現(xiàn)錯(cuò)誤時(shí),通過 completion. Handler烤惊,或通過調(diào)用委托方法(在創(chuàng)建會(huì)話時(shí)設(shè)置的)乔煞。
現(xiàn)在我們已經(jīng)了解了 NSURLSession 可以做的事,是時(shí)候?qū)⒗碚摳吨T實(shí)現(xiàn)了柒室!
查詢曲目
我們將首先在用戶搜索曲目時(shí)添加查詢 iTunes Search API 的代碼渡贾。
在 SearchViewController.swift 中,將如下代碼添加到類的頂部:
// 1
let defaultSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
// 2
var dataTask: NSURLSessionDataTask?
上面的代碼我們做了這些事:
- 創(chuàng)建了 NSURLSession 然后用默認(rèn)會(huì)話配置初始化它雄右。
- 聲明了一個(gè) NSURLSessionDataTask 變量空骚,在用戶執(zhí)行搜索時(shí)用于向 iTunes Search 網(wǎng)絡(luò)服務(wù)發(fā)出 HTTP GET 請(qǐng)求。data task 會(huì)在每次用戶創(chuàng)建新查詢的時(shí)候被重新初始化和復(fù)用擂仍。
現(xiàn)在囤屹,用如下代碼替換掉 searchBarSearchButtonClicked(_:):
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
dismissKeyboard()
if !searchBar.text!.isEmpty {
// 1
if dataTask != nil {
dataTask?.cancel()
}
// 2
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
// 3
let expectedCharSet = NSCharacterSet.URLQueryAllowedCharacterSet()
let searchTerm = searchBar.text!.stringByAddingPercentEncodingWithAllowedCharacters(expectedCharSet)!
// 4
let url = NSURL(string: "https://itunes.apple.com/search?media=music&entity=song&term=\(searchTerm)")
// 5
dataTask = defaultSession.dataTaskWithURL(url!) {
data, response, error in
// 6
dispatch_async(dispatch_get_main_queue()) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
// 7
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? NSHTTPURLResponse {
if httpResponse.statusCode == 200 {
self.updateSearchResults(data)
}
}
}
// 8
dataTask?.resume()
}
}
按順序討論一下每個(gè)數(shù)字注釋:
- 每次用戶查詢時(shí),檢查 data task 是否已經(jīng)被初始化了逢渔。如果是牺丙,就取消之,因?yàn)槲覀円獜?fù)用這個(gè) Data task 對(duì)象以用于最新的查詢复局。
- 在狀態(tài)欄上啟用網(wǎng)絡(luò)活動(dòng) indicator冲簿,以向用戶指明有一個(gè)正在進(jìn)行的網(wǎng)絡(luò)進(jìn)程。
- 在將用戶的搜索字符串作為參數(shù)傳遞給查詢 URL 之前亿昏,在該字符串上調(diào)用
stringByAddingPercentEncodingWithAllowedCharacters(_:)
以確保被正確轉(zhuǎn)義了峦剔。 - 下一步我們構(gòu)造了一個(gè) NSURL,將轉(zhuǎn)義后的搜索字符串作為 GET 參數(shù)附加到 iTunes Search API 的 base url 上角钩。
- 從我們創(chuàng)建的會(huì)話中初始化一個(gè) NSURLSessionDataTask 以處理 HTTP GET 請(qǐng)求吝沫。NSURLSessionDataTask 的構(gòu)造器帶有一個(gè) NSURL 參數(shù)以及一個(gè) completion handler,以供 data task 完成時(shí)調(diào)用递礼。
- 如果收到了任務(wù)完成的回調(diào)惨险,在主線程中隱藏 activity indicator 并調(diào)用 UI 刷新。
- 如果 HTTP 請(qǐng)求成功了脊髓,調(diào)用 updateSearchResults(_:)辫愉,它將 response NSData 解析為 Tracks 然后更新 table view。
- 默認(rèn)情況下将硝,所有任務(wù)剛開始時(shí)都是暫停狀態(tài)恭朗;調(diào)用 resume() 以啟動(dòng) data task。
構(gòu)建并運(yùn)行 app依疼;搜索任意音樂痰腮,可以看到 table view 充滿了相關(guān)的曲目結(jié)果,如下所示:
在我們注入了一點(diǎn) NSURLSession 魔法后,Half Tunes 現(xiàn)在已經(jīng)有點(diǎn)用處了!
下載曲目
能夠看到歌曲結(jié)果是一件很爽的事,但如果點(diǎn)擊歌曲就能下載豈不是更棒棒茉兰?這就是我們接下來要做的事情沧踏。
為了能夠處理多個(gè)下載歌逢,首先創(chuàng)建一個(gè)自定義對(duì)象以管理活動(dòng)下載的狀態(tài)。
在 Data Objects 組中創(chuàng)建一個(gè)新文件悦冀,命名為 Download.swift趋翻。
打開 Download.swift 然后添加如下實(shí)現(xiàn):
class Download: NSObject {
var url: String
var isDownloading = false
var progress: Float = 0.0
var downloadTask: NSURLSessionDownloadTask?
var resumeData: NSData?
init(url: String) {
self.url = url
}
}
梳理一下 Download 的屬性:
- url:需要下載文件的 URL睛琳。也是 Download 的唯一標(biāo)識(shí)符盒蟆。
- isDownloading:下載是在進(jìn)行中還是被暫停了。
- progress:下載的小數(shù)進(jìn)度师骗;介于 0.0 和 1.0 之間的 float 型历等。
- downloadTask:下載文件的 NSURLSessionDownloadTask。
- resumeData:存儲(chǔ)在暫停下載任務(wù)時(shí)生成的 NSData辟癌。如果主機(jī)服務(wù)器支持寒屯,可以用它來恢復(fù)暫停的下載。
切換到 SearchViewController.swift 然后在類頂部添加如下代碼:
var activeDownloads = [String: Download]()
維持 URL 和活躍下載之間的映射而已黍少。
創(chuàng)建下載任務(wù)
準(zhǔn)備工作都結(jié)束了寡夹,現(xiàn)在可以實(shí)現(xiàn)文件下載了。首先創(chuàng)建一個(gè)專用會(huì)話以處理下載任務(wù)厂置。
在 SearchViewController.swift 中菩掏,在 viewDidLoad() 之前添加如下代碼:
lazy var downloadsSession: NSURLSession = {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
return session
}()
這里我們用默認(rèn)配置初始化了一個(gè)單獨(dú)的會(huì)話以處理所有下載任務(wù)。還指定了一個(gè) delegate昵济,以通過委托調(diào)用接收 NSURLSession 事件智绸。這樣會(huì)很有用,不僅可以追蹤任務(wù)完成访忿,還有任務(wù)進(jìn)度瞧栗。
將 delegate queue 設(shè)置為 nil 會(huì)讓會(huì)話創(chuàng)建一個(gè)串行操作隊(duì)列(默認(rèn)值)以執(zhí)行對(duì)委托方法和 completion handlers 的調(diào)用。
注意 downloadsSession: 的 lazy 創(chuàng)建:可以把會(huì)話的創(chuàng)建延遲到真正需要使用它的時(shí)刻海铆。最重要的是迹恐,可以把 self 作為 delegate 參數(shù)傳給初始化程序 —— 即使 self 并未并初始化。
在 SearchViewController.swift 中卧斟,找到空的 NSURLSessionDownloadDelegate 擴(kuò)展然后改成這樣:
extension SearchViewController: NSURLSessionDownloadDelegate {
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
print("Finished downloading.")
}
}
NSURLSessionDownloadDelegate 定義了使用 NSURLSession 下載任務(wù)時(shí)需要實(shí)現(xiàn)的代理方法系草。唯一不可或缺的方法是 URLSession(_:downloadTask:didFinishDownloadingToURL:),下載完成時(shí)調(diào)用∷衾裕現(xiàn)在我們就在下載完成時(shí)打印一條消息就可以了找都。
會(huì)話和 delegate 都配置好后,重要可以開始在用戶請(qǐng)求下載歌曲的時(shí)候創(chuàng)建下載任務(wù)了廊酣。
在 SearchViewController.swift 中能耻,將 startDownload(_:)
替換為如下實(shí)現(xiàn):
func startDownload(track: Track) {
if let urlString = track.previewUrl, url = NSURL(string: urlString) {
// 1
let download = Download(url: urlString)
// 2
download.downloadTask = downloadsSession.downloadTaskWithURL(url)
// 3
download.downloadTask!.resume()
// 4
download.isDownloading = true
// 5
activeDownloads[download.url] = download
}
}
用戶點(diǎn)擊某個(gè)曲目的 Download 按鈕的時(shí)候,帶上相應(yīng)的 Track 調(diào)用 startDownload(_:)。解釋如下:
- 首先用 track 的 preview URL 來初始化 Download晓猛。
- 使用新的會(huì)話對(duì)象創(chuàng)建帶有 preview URL 的 NSURLSessionDownloadTask饿幅,然后將其設(shè)置為 Download 的 downloadTask 屬性。
- 調(diào)用 resume() 以啟動(dòng)下載任務(wù)戒职。
- 指示下載正在進(jìn)行中栗恩。
- 最后,在 activeDownloads 字典中將下載 URL 映射到 Download洪燥。
構(gòu)建并運(yùn)行 app磕秤;搜索任意歌曲然后點(diǎn)擊單元格上的 Download 按鈕。一段時(shí)間后捧韵,應(yīng)該會(huì)在控制臺(tái)上看到一條消息市咆,表示下載完成。
保存和播放曲目
下載任務(wù)完成后再来,URLSession(_:downloadTask:didFinishDownloadingToURL:) 會(huì)提供臨時(shí)文件位置 URL蒙兰。方法返回前,我們要將其移動(dòng)到 app 沙箱容器目錄中的永久位置芒篷。還有搜变,必須從字典中移除活動(dòng)下載并更新 table view。
添加一個(gè)幫助方法以簡(jiǎn)化這個(gè)步驟针炉。在 SearchViewController.swift 中挠他,將以下方法添加到類中:
func trackIndexForDownloadTask(downloadTask: NSURLSessionDownloadTask) -> Int? {
if let url = downloadTask.originalRequest?.URL?.absoluteString {
for (index, track) in searchResults.enumerate() {
if url == track.previewUrl! {
return index
}
}
}
return nil
}
該方法只返回給定 URL 的 Track 在 searchResults 列表中的索引。
下一步糊识,將 URLSession(_:downloadTask:didFinishDownloadingToURL:) 替換為如下代碼:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
// 1
if let originalURL = downloadTask.originalRequest?.URL?.absoluteString,
destinationURL = localFilePathForUrl(originalURL) {
print(destinationURL)
// 2
let fileManager = NSFileManager.defaultManager()
do {
try fileManager.removeItemAtURL(destinationURL)
} catch {
// Non-fatal: file probably doesn't exist
}
do {
try fileManager.copyItemAtURL(location, toURL: destinationURL)
} catch let error as NSError {
print("Could not copy file to disk: \(error.localizedDescription)")
}
}
// 3
if let url = downloadTask.originalRequest?.URL?.absoluteString {
activeDownloads[url] = nil
// 4
if let trackIndex = trackIndexForDownloadTask(downloadTask) {
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: trackIndex, inSection: 0)], withRowAnimation: .None)
})
}
}
}
上面的關(guān)鍵步驟解釋如下:
- 從任務(wù)中提取出原始請(qǐng)求 URL 然后將其傳遞給 localFilePathForUrl(:) 幫助方法绩社。localFilePathForUrl(:) 隨后就會(huì)生成一個(gè)用來存儲(chǔ)的永久本地文件路徑(通過將 URL 的 lastPathComponent(即文件名和擴(kuò)展名)附加到 app 的 Documents 路徑上)。
- 使用 NSFileManager赂苗,將下載的文件從臨時(shí)文件位置移動(dòng)到所需的目的文件路徑(開始復(fù)制之前愉耙,清楚那個(gè)位置上的文件)。
- 在活躍下載中查找相應(yīng)的 Download 并將其移除拌滋。
- 最后朴沿,在 table view 里找到那個(gè) Track 然后重載相應(yīng)的單元格。
構(gòu)建并運(yùn)行項(xiàng)目败砂;選擇任意曲目然后下載它赌渣。下載完成時(shí),應(yīng)該可以看到控制臺(tái)中打出了文件路徑位置:
下載按鈕也會(huì)消失昌犹,因?yàn)榍楷F(xiàn)在已經(jīng)在設(shè)備上了坚芜。點(diǎn)擊曲目就會(huì)在顯示的 MPMoviePlayerViewController 中聽到它播放,如下所示:
監(jiān)測(cè)下載進(jìn)度
目前斜姥,我們無法監(jiān)控下載進(jìn)度鸿竖。為了改善用戶體驗(yàn)沧竟,我們會(huì)改動(dòng) app 以監(jiān)聽下載進(jìn)度事件,并在單元格中顯示進(jìn)度缚忧。
在 SearchViewController.swift 中悟泵,找到那個(gè)實(shí)現(xiàn) NSURLSessionDownloadDelegate 的擴(kuò)展,然后添加如下代理方法:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
// 1
if let downloadUrl = downloadTask.originalRequest?.URL?.absoluteString,
download = activeDownloads[downloadUrl] {
// 2
download.progress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
// 3
let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: NSByteCountFormatterCountStyle.Binary)
// 4
if let trackIndex = trackIndexForDownloadTask(downloadTask), let trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: trackIndex, inSection: 0)) as? TrackCell {
dispatch_async(dispatch_get_main_queue(), {
trackCell.progressView.progress = download.progress
trackCell.progressLabel.text = String(format: "%.1f%% of %@", download.progress * 100, totalSize)
})
}
}
}
一步步瀏覽這個(gè)代理方法:
- 使用提供的 downloadTask闪水,取出 URL 然后用它在活躍下載目錄中找到 Download糕非。
- 該方法還返回寫入的總字節(jié)數(shù)以及預(yù)期寫入的總字節(jié)數(shù)。計(jì)算兩個(gè)值的比值就是進(jìn)度球榆,然后將結(jié)果保存到 Download 中朽肥。我們會(huì)使用這個(gè)值來更新 progress view。
- NSByteCountFormatter 帶有一個(gè)字節(jié)值參數(shù)芜果,然后生成友好的下載文件總尺寸的字符串鞠呈。我們會(huì)使用此字符串來顯示下載的大小以及完成百分比融师。
- 最后右钾,找到負(fù)責(zé)顯示這個(gè) Track 的單元格,然后同時(shí)刷新其進(jìn)度視圖與進(jìn)度 label(借助前面步驟得到的值)旱爆。
下一步舀射,配置單元格以正確顯示進(jìn)行中的下載的 progress view 和狀態(tài)。
在 tableView(_:cellForRowAtIndexPath:) 中找到如下代碼行:
let downloaded = localFileExistsForTrack(track)
在上面那行前面添加如下代碼:
var showDownloadControls = false
if let download = activeDownloads[track.previewUrl!] {
showDownloadControls = true
cell.progressView.progress = download.progress
cell.progressLabel.text = (download.isDownloading) ? "Downloading..." : "Paused"
}
cell.progressView.hidden = !showDownloadControls
cell.progressLabel.hidden = !showDownloadControls
對(duì)于有活躍下載的曲目怀伦,將 showDownloadControls 設(shè)置為 true脆烟;否則,將其設(shè)置為 false房待。然后根據(jù) showDownloadControls 的值來顯示進(jìn)度視圖和標(biāo)簽(示例項(xiàng)目中已提供)邢羔。
對(duì)于被暫停的下載,狀態(tài)顯示為 “Paused”桑孩;否則拜鹤,顯示 “Downloading…”。
最后流椒,將下面這行:
cell.downloadButton.hidden = downloaded
替換為:
cell.downloadButton.hidden = downloaded || showDownloadControls
在這里敏簿,如果曲目正在下載也要隱藏 Download 按鈕。
構(gòu)建并運(yùn)行項(xiàng)目宣虾;下載任意曲目惯裕,應(yīng)該看到隨著下載進(jìn)行,進(jìn)度條狀態(tài)的更新:
棒棒的绣硝,我們有了重大進(jìn)展蜻势!:]
暫停、恢復(fù)和取消下載
如果需要暫宛呐郑或完全取消下載握玛,該怎么做猜煮?在本節(jié)中,我們會(huì)實(shí)現(xiàn)暫停败许、恢復(fù)和取消功能王带,使用戶能夠完全控制下載過程。
先從讓用戶取消活躍下載開始市殷。
替換 cancelDownload(_:) 為如下代碼:
func cancelDownload(track: Track) {
if let urlString = track.previewUrl,
download = activeDownloads[urlString] {
download.downloadTask?.cancel()
activeDownloads[urlString] = nil
}
}
為了取消下載愕撰,從活動(dòng)下載字典中相應(yīng)的 Download 中取出下載任務(wù),然后對(duì)其調(diào)用 cancel() 以取消任務(wù)醋寝。然后從活動(dòng)下載字典中移除它搞挣。
暫停下載在概念上和取消很相似;區(qū)別在于暫停會(huì)取消下載任務(wù)音羞,但也會(huì)產(chǎn)生恢復(fù)數(shù)據(jù)囱桨,其中包含足夠的信息以在未來恢復(fù)下載,需要主機(jī)服務(wù)器支持該功能嗅绰。
注意:只能在特定情況下恢復(fù)下載舍肠。例如,首次請(qǐng)求后窘面,資源不能被修改翠语。有關(guān)情況的完整列表,請(qǐng)?jiān)?這里 查看蘋果文檔财边。
現(xiàn)在肌括,將 pauseDownload(_:) 替換為如下代碼:
func pauseDownload(track: Track) {
if let urlString = track.previewUrl,
download = activeDownloads[urlString] {
if(download.isDownloading) {
download.downloadTask?.cancelByProducingResumeData { data in
if data != nil {
download.resumeData = data
}
}
download.isDownloading = false
}
}
}
這里的主要區(qū)別是調(diào)用 cancelByProducingResumeData(:) 而不是 cancel()。從 cancelByProducingResumeData(:) 提供的閉包中取回恢復(fù)數(shù)據(jù)酣难,然后將其存儲(chǔ)到合適的 Download 中以備將來恢復(fù)谍夭。
我們還將 Download 的 isDownloading 屬性設(shè)置為 false 以表示下載被暫停了。
暫停功能完成后憨募,下面我們要恢復(fù)被暫停的下載紧索。
將 resumeDownload(_:) 替換為如下代碼:
func resumeDownload(track: Track) {
if let urlString = track.previewUrl,
download = activeDownloads[urlString] {
if let resumeData = download.resumeData {
download.downloadTask = downloadsSession.downloadTaskWithResumeData(resumeData)
download.downloadTask!.resume()
download.isDownloading = true
} else if let url = NSURL(string: download.url) {
download.downloadTask = downloadsSession.downloadTaskWithURL(url)
download.downloadTask!.resume()
download.isDownloading = true
}
}
}
當(dāng)用戶恢復(fù)下載時(shí),檢查相應(yīng)的 Download 是否存在恢復(fù)數(shù)據(jù)馋嗜。如果有齐板,帶上恢復(fù)數(shù)據(jù)調(diào)用 downloadTaskWithResumeData(_:) 以創(chuàng)建一個(gè)新的下載任務(wù),然后調(diào)用 resume() 來啟動(dòng)任務(wù)葛菇。如果由于某些原因缺少恢復(fù)數(shù)據(jù)甘磨,就從從頭開始創(chuàng)建一個(gè)新的下載任務(wù),并使用下載 URL 啟動(dòng)它眯停。
兩種情況下济舆,都把 Download 的 isDownloading 標(biāo)志設(shè)置為 true 來表示下載被恢復(fù)了。
還需要再做一件事莺债,這三個(gè)功能就能正常工作了:我們需要按需顯示或隱藏 Pause滋觉、Cancel 和 Resume 按鈕签夭。
跳到 tableView(_:cellForRowAtIndexPath:) 然后找到下面這行:
if let download = activeDownloads[track.previewUrl!] {
把下面兩行代碼添加到上面那個(gè) let 代碼塊的底部:
let title = (download.isDownloading) ? "Pause" : "Resume"
cell.pauseButton.setTitle(title, forState: UIControlState.Normal)
因?yàn)闀和:突謴?fù)功能用同一個(gè)按鈕,上面的代碼就按需在兩個(gè)狀態(tài)間切換按鈕椎侠。
下面第租,把下面的代碼添加到 tableView(_:cellForRowAtIndexPath:) 的末端,return 語句之前:
cell.pauseButton.hidden = !showDownloadControls
cell.cancelButton.hidden = !showDownloadControls
這個(gè) app 中我纪,只有下載活躍時(shí)才會(huì)顯示按鈕慎宾。
構(gòu)建并運(yùn)行項(xiàng)目;同時(shí)下載幾個(gè)曲目浅悉,你可以隨意暫停趟据、恢復(fù)和取消它們:
啟用后臺(tái)傳輸
我們的 app 目前功能已經(jīng)很強(qiáng)大了,但還可以做一個(gè)重大的改進(jìn):后臺(tái)傳輸术健。在這種模式下汹碱,即使 app 在后臺(tái)或因某原因崩潰掉時(shí),下載還是會(huì)繼續(xù)荞估。
但是如果 app 都沒有運(yùn)行咳促,那怎么工作呢?有一個(gè)在 app 外部單獨(dú)運(yùn)行的守護(hù)進(jìn)程泼舱,管理后臺(tái)傳輸任務(wù)等缀;當(dāng)運(yùn)行下載任務(wù)時(shí)枷莉,它會(huì)將適當(dāng)?shù)?delegate 消息發(fā)送到 app娇昙。如果 app 在前臺(tái)傳輸期間終止運(yùn)行了,任務(wù)將在后臺(tái)繼續(xù)運(yùn)行笤妙,不受影響冒掌。
任務(wù)完成后,守護(hù)進(jìn)程將在后臺(tái)重新啟動(dòng)該 app蹲盘。被重新啟動(dòng)的 app 會(huì)再次連接到那個(gè)會(huì)話股毫,接收相關(guān)的 completion delegate 消息并執(zhí)行任何必須的操作,例如將下載的文件持久化存儲(chǔ)到硬盤召衔。
注意:如果從 app 切換器 強(qiáng)制退出 app铃诬,系統(tǒng)會(huì)取消所有會(huì)話的后臺(tái)傳輸,并不會(huì)嘗試重啟 app苍凛。
還是在 SearchViewController.swift趣席,downloadsSession 的初始化方法里,找到下面這行代碼:
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
……替換為下面這行:
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("bgSessionConfiguration")
沒有用默認(rèn)會(huì)話配置醇蝴,我們可以用一種特殊的后臺(tái)會(huì)話配置宣肚。請(qǐng)注意,我們還可以在此處設(shè)置會(huì)話的唯一標(biāo)識(shí)符悠栓,以便在需要時(shí)引用并“重新連接”到相同的后臺(tái)會(huì)話霉涨。
接下來按价,在 viewDidLoad() 中,添加下面這行:
_ = self.downloadsSession
調(diào)用 lazy 加載的 downloadsSession 可以確保 app 在 SearchViewController 初始化時(shí)創(chuàng)建一個(gè)后臺(tái)會(huì)話笙瑟。
如果后臺(tái)任務(wù)在 app 未運(yùn)行時(shí)完成楼镐,該應(yīng)用將在后臺(tái)重新啟動(dòng)。我們需要在 app delegate 中處理這個(gè)事件往枷。
切至 AppDelegate.swift 然后在類頂部添加下面這行代碼:
var backgroundSessionCompletionHandler: (() -> Void)?
下一步鸠蚪,把下面的方法添加到 AppDelegate.swift:
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
這兒我們把提供的 completionHandler 以變量形式保存在 app delegate 以備后面使用。
application(_:handleEventsForBackgroundURLSession:) 會(huì)喚醒 app 來處理完成的后臺(tái)任務(wù)师溅。這個(gè)事件中需要處理兩件事情:
- 首先茅信,app 需要使用 delegate 方法提供的標(biāo)識(shí)符重新連接到相應(yīng)的后臺(tái)會(huì)話。但是墓臭,由于每次實(shí)例化 SearchViewController 時(shí)都會(huì)創(chuàng)建并使用一個(gè)后臺(tái)會(huì)話蘸鲸,因此這時(shí)已經(jīng)重新連接了!
- 其次窿锉,需要捕捉由 delegate 方法提供的 completion handler酌摇。調(diào)用 completion handler 會(huì)使操作系統(tǒng)對(duì)更新后的 UI 進(jìn)行快照,以便在應(yīng)用程序切換器中顯示嗡载,并告訴操作系統(tǒng)窑多,app 當(dāng)前會(huì)話的后臺(tái)活動(dòng)都已完成。
但是應(yīng)該何時(shí)調(diào)用 completion handler洼滚?URLSessionDidFinishEventsForBackgroundURLSession(_:) 會(huì)是一個(gè)不錯(cuò)的選擇埂息;這時(shí)一個(gè) NSURLSessionDelegate 方法,與后臺(tái)會(huì)話相關(guān)的所有任務(wù)都完成時(shí)會(huì)觸發(fā)遥巴。
在 SearchViewController.swift 中實(shí)現(xiàn)下面的擴(kuò)展:
extension SearchViewController: NSURLSessionDelegate {
func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) {
if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate {
if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
dispatch_async(dispatch_get_main_queue(), {
completionHandler()
})
}
}
}
}
上面的代碼知識(shí)從 app delegate 中獲取存儲(chǔ)的 completion handler千康,并在主線程上調(diào)用它。
構(gòu)建并運(yùn)行 app铲掐;啟動(dòng)幾次并發(fā)下載拾弃,然后點(diǎn) home 鍵讓 app 在后臺(tái)運(yùn)行。等待下載完成摆霉,然后雙擊 home 鍵顯示 app 切換器豪椿。
下載應(yīng)該已經(jīng)完成,它們新的狀態(tài)反映在 app 截圖中携栋。打開 app 確認(rèn)一下:
現(xiàn)在我們已經(jīng)有了一個(gè)功能齊全的音樂流媒體 app搭盾!下一步是挑戰(zhàn) Apple Music!:]
下一步刻两?
這里 可以下載本教程的完整項(xiàng)目增蹭。
恭喜!你現(xiàn)在已具備處理 app 中大多數(shù)常見網(wǎng)絡(luò)需求的能力磅摹。NSURLSession 還有很多細(xì)節(jié)滋迈,這篇 NSURLSession 教程中裝不下了霎奢,例如上傳任務(wù)和會(huì)話配置設(shè)置(如超時(shí)值和緩存策略)。
要了解有關(guān)這些功能(和其他功能)的更多信息饼灿,請(qǐng)查看以下資源:
- 蘋果 文檔 幕侠,包含所有 API 方法的細(xì)節(jié)。
- 我們自己的 iOS 7 By Tutorials 書碍彭,整整兩章都是專門講 NSURLSession晤硕。還可以看看我們之前的 NSURLSession 教程。
- AlamoFire 是一個(gè)流行的第三方 iOS 網(wǎng)絡(luò)庫庇忌;我們?cè)?Beginning Alamofire 教程中講了基礎(chǔ)部分舞箍。
希望你能用上這篇教程。隨便在下面評(píng)論吧皆疹!