詳解 NSURLSession

本文翻譯自 NSURLSession Tutorial: Getting Started


學(xué)習(xí)如何用 NSURLSession 實(shí)現(xiàn) HTTP 數(shù)據(jù)請(qǐng)求和文件下載!

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?

上面的代碼我們做了這些事:

  1. 創(chuàng)建了 NSURLSession 然后用默認(rèn)會(huì)話配置初始化它雄右。
  2. 聲明了一個(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ù)字注釋:

  1. 每次用戶查詢時(shí),檢查 data task 是否已經(jīng)被初始化了逢渔。如果是牺丙,就取消之,因?yàn)槲覀円獜?fù)用這個(gè) Data task 對(duì)象以用于最新的查詢复局。
  2. 在狀態(tài)欄上啟用網(wǎng)絡(luò)活動(dòng) indicator冲簿,以向用戶指明有一個(gè)正在進(jìn)行的網(wǎng)絡(luò)進(jìn)程。
  3. 在將用戶的搜索字符串作為參數(shù)傳遞給查詢 URL 之前亿昏,在該字符串上調(diào)用 stringByAddingPercentEncodingWithAllowedCharacters(_:) 以確保被正確轉(zhuǎn)義了峦剔。
  4. 下一步我們構(gòu)造了一個(gè) NSURL,將轉(zhuǎn)義后的搜索字符串作為 GET 參數(shù)附加到 iTunes Search API 的 base url 上角钩。
  5. 從我們創(chuàng)建的會(huì)話中初始化一個(gè) NSURLSessionDataTask 以處理 HTTP GET 請(qǐng)求吝沫。NSURLSessionDataTask 的構(gòu)造器帶有一個(gè) NSURL 參數(shù)以及一個(gè) completion handler,以供 data task 完成時(shí)調(diào)用递礼。
  6. 如果收到了任務(wù)完成的回調(diào)惨险,在主線程中隱藏 activity indicator 并調(diào)用 UI 刷新。
  7. 如果 HTTP 請(qǐng)求成功了脊髓,調(diào)用 updateSearchResults(_:)辫愉,它將 response NSData 解析為 Tracks 然后更新 table view。
  8. 默認(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(_:)。解釋如下:

  1. 首先用 track 的 preview URL 來初始化 Download晓猛。
  2. 使用新的會(huì)話對(duì)象創(chuàng)建帶有 preview URL 的 NSURLSessionDownloadTask饿幅,然后將其設(shè)置為 Download 的 downloadTask 屬性。
  3. 調(diào)用 resume() 以啟動(dòng)下載任務(wù)戒职。
  4. 指示下載正在進(jìn)行中栗恩。
  5. 最后,在 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)鍵步驟解釋如下:

  1. 從任務(wù)中提取出原始請(qǐng)求 URL 然后將其傳遞給 localFilePathForUrl(:) 幫助方法绩社。localFilePathForUrl(:) 隨后就會(huì)生成一個(gè)用來存儲(chǔ)的永久本地文件路徑(通過將 URL 的 lastPathComponent(即文件名和擴(kuò)展名)附加到 app 的 Documents 路徑上)。
  2. 使用 NSFileManager赂苗,將下載的文件從臨時(shí)文件位置移動(dòng)到所需的目的文件路徑(開始復(fù)制之前愉耙,清楚那個(gè)位置上的文件)。
  3. 在活躍下載中查找相應(yīng)的 Download 并將其移除拌滋。
  4. 最后朴沿,在 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è)代理方法:

  1. 使用提供的 downloadTask闪水,取出 URL 然后用它在活躍下載目錄中找到 Download糕非。
  2. 該方法還返回寫入的總字節(jié)數(shù)以及預(yù)期寫入的總字節(jié)數(shù)。計(jì)算兩個(gè)值的比值就是進(jìn)度球榆,然后將結(jié)果保存到 Download 中朽肥。我們會(huì)使用這個(gè)值來更新 progress view。
  3. NSByteCountFormatter 帶有一個(gè)字節(jié)值參數(shù)芜果,然后生成友好的下載文件總尺寸的字符串鞠呈。我們會(huì)使用此字符串來顯示下載的大小以及完成百分比融师。
  4. 最后右钾,找到負(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)論吧皆疹!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疏橄,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子略就,更是在濱河造成了極大的恐慌捎迫,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件表牢,死亡現(xiàn)場(chǎng)離奇詭異窄绒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)崔兴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門彰导,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恼布,你說我怎么就攤上這事螺戳。” “怎么了折汞?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盖腿。 經(jīng)常有香客問我爽待,道長,這世上最難降的妖魔是什么翩腐? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任鸟款,我火速辦了婚禮,結(jié)果婚禮上茂卦,老公的妹妹穿的比我還像新娘何什。我一直安慰自己,他們只是感情好等龙,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布处渣。 她就那樣靜靜地躺著伶贰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪罐栈。 梳的紋絲不亂的頭發(fā)上黍衙,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音荠诬,去河邊找鬼琅翻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛柑贞,可吹牛的內(nèi)容都是我干的方椎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼钧嘶,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼辩尊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起康辑,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤摄欲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后疮薇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胸墙,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年按咒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了迟隅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡励七,死狀恐怖智袭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掠抬,我是刑警寧澤吼野,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站两波,受9級(jí)特大地震影響瞳步,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腰奋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一单起、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧劣坊,春花似錦嘀倒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灌危。三九已至,卻和暖如春帮寻,著一層夾襖步出監(jiān)牢的瞬間乍狐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來泰國打工固逗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浅蚪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓烫罩,卻偏偏與公主長得像惜傲,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贝攒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容