热门搜索 :
考研考公
您的当前位置:首页正文

详细解析几个和网络请求有关的类(二十七) —— NSURLSes

来源:东饰资讯网

版本记录

版本号 时间
V1.0 2019.06.15 星期六

前言

开始

首先看下写作环境

Swift 5, iOS 12, Xcode 10

在此URLSession教程中,您将学习如何创建HTTP请求以及实现可以暂停和恢复的后台下载。

无论应用程序从服务器检索应用程序数据,更新您的社交媒体状态还是将远程文件下载到磁盘,网络请求都是让奇迹发生的原因。 为了帮助您满足网络请求的许多要求,Apple提供了URLSession,这是一个用于上传和下载内容的完整网络API。

打开下载好的入门项目,它包含用于搜索歌曲和显示搜索结果的用户界面,具有一些存根功能的网络类以及用于存储和播放曲目的辅助方法。 这使您可以专注于实现应用程序的网络方面。

构建并运行项目。 您将在顶部看到一个带有搜索栏的视图,下面是一个空表视图:

在搜索栏中键入查询,然后点按Search。 视图仍然是空的。 不过不用担心,您将使用新的URLSession调用更改此情况。


URLSession Overview

在开始之前,了解URLSession及其组成类非常重要,因此请查看下面的快速概述。

URLSession既是一类,也是一套用于处理基于HTTPHTTPS的请求的类:

URLSession是负责发送和接收请求的关键对象。 您可以通过URLSessionConfiguration创建它,它有三种形式:

  • default:创建使用磁盘持久全局缓存,凭据和cookie存储对象的默认配置对象。
  • ephemeral:与default配置类似,不同之处在于您将所有与会话相关的数据存储在内存中。 将此视为“私人”会话。
  • background:允许会话在后台执行上载或下载任务。 即使应用程序本身被系统暂停或终止,传输仍会继续。

URLSessionTask是一个表示任务对象的抽象类。 会话创建一个或多个任务来执行获取数据和下载或上载文件的实际工作。

1. Understanding Session Task Types

有三种类型的具体会话任务:

  • URLSessionDataTask:将此任务用于GET请求,以将数据从服务器检索到内存。
  • URLSessionUploadTask:使用此任务通过POSTPUT方法将文件从磁盘上载到Web服务。
  • URLSessionDownloadTask:使用此任务将文件从远程服务下载到临时文件位置。

您还可以暂停,恢复和取消任务。 URLSessionDownloadTask具有暂停以供将来恢复的额外功能。

通常,URLSession以两种方式返回数据:

  • 当任务完成时,成功或出错都会走completion handler
  • 通过调用在创建会话时设置的代理上的方法。

既然您已经了解了URLSession可以做什么,那么您已准备好将理论付诸实践!


DataTask and DownloadTask

首先,您将创建一个数据任务,以便在iTunes Search API中查询用户的搜索词。

SearchViewController.swift中,searchBarSearchButtonClicked启用状态栏上的网络活动指示器,以向用户显示网络进程正在运行。 然后它调用getSearchResults(searchTerm:completion :),它在QueryService.swift中被删除。 您即将构建它以发出网络请求。

QueryService.swift中,使用以下内容替换// TODO 1

let defaultSession = URLSession(configuration: .default)

// TODO 2用下面替换

var dataTask: URLSessionDataTask?

这就是你所做的:

  • 1) 创建了一个URLSession并使用default会话配置对其进行了初始化。
  • 2) 声明了URLSessionDataTask,用于在用户执行搜索时向iTunes搜索Web服务发出GET请求。 每次用户输入新的搜索字符串时,都会重新初始化数据任务。

接下来,使用以下内容替换getSearchResults(searchTerm:completion :)中的内容:

// 1
dataTask?.cancel()
    
// 2
if var urlComponents = URLComponents(string:  {
  urlComponents.query = "media=music&entity=song&term=\(searchTerm)"      
  // 3
  guard let url = urlComponents.url else {
    return
  }
  // 4
  dataTask = 
    defaultSession.dataTask(with: url) { [weak self] data, response, error in 
    defer {
      self?.dataTask = nil
    }
    // 5
    if let error = error {
      self?.errorMessage += "DataTask error: " + 
                              error.localizedDescription + "\n"
    } else if 
      let data = data,
      let response = response as? HTTPURLResponse,
      response.statusCode == 200 {       
      self?.updateSearchResults(data)
      // 6
      DispatchQueue.main.async {
        completion(self?.tracks, self?.errorMessage ?? "")
      }
    }
  }
  // 7
  dataTask?.resume()
}

依次记录每个编号的评论:

  • 1) 对于新用户查询,您将取消已存在的任何数据任务,因为您要为此新查询重用数据任务对象。
  • 2) 要在查询URL中包含用户的搜索字符串,请从iTunes Search base URL创建URLComponents,然后设置其查询字符串。这可确保您的搜索字符串使用转义字符。
  • 3) urlComponentsurl属性是可选的,因此您将其解包为url并在它为nil时提前return
  • 4) 在您创建的会话中,使用查询URL初始化URLSessionDataTask,并在数据任务完成时调用完成处理程序。
  • 5) 如果请求成功,则调用辅助方法updateSearchResults,该方法将响应data解析为tracks数组。
  • 6) 切换到主队列以将tracks传递给completion handler
  • 7) 默认情况下,所有任务都以挂起状态启动。调用resume()启动数据任务。

SearchViewController中,查看对getSearchResults(searchTerm:completion :)的调用中的完成闭包。隐藏活动指示器后,它会将results存储在searchResults中,然后更新表视图。

注意:默认请求方法是GET。如果要将数据任务设置为POST,PUT或DELETE,请使用url创建URLRequest,设置请求的HTTPMethod属性,然后使用URLRequest而不是URL创建数据任务。

构建并运行您的应用程序。搜索任何歌曲,您将看到表格视图填充相关的曲目结果,如下所示:

有了一些URLSession代码,Half Tunes现在有点功能了!

能够查看歌曲结果很不错,但如果你可以点击一首歌下载它会不会更好? 这是你的下一个业务订单。 您将使用download task,这样可以轻松地将歌曲片段保存在本地文件中。

1. Downloading Classes

处理多次下载需要做的第一件事是创建一个自定义对象来保存活动下载的状态。

Model组中创建一个名为Download.swift的新Swift文件。

打开Download.swift,并在Foundation导入下面添加以下实现:

class Download {
  var isDownloading = false
  var progress: Float = 0
  var resumeData: Data?
  var task: URLSessionDownloadTask?
  var track: Track
  
  init(track: Track) {
    self.track = track
  }
}

这是Download属性的简要说明:

  • isDownloading:下载是正在进行还是暂停。
  • progress:下载的小数进度,表示为介于0.0和1.0之间的浮点数。
  • resumeData:存储用户暂停下载任务时生成的Data。 如果主机服务器支持它,您的应用程序可以使用它来恢复暂停的下载。
  • task:下载trackURLSessionDownloadTask
  • track:要下载的曲目。 trackurl属性也充当Download的唯一标识符。

接下来,在DownloadService.swift中,将// TODO 4替换为以下属性:

var activeDownloads: [URL: Download] = [:]

该字典将维护URL与其活动Download之间的映射(如果有)。


URLSession Delegates

您可以使用完成处理程序completion handler创建下载任务,就像创建数据任务data task时一样。 但是,在本教程的后面部分,您将检查并更新下载进度,这需要您实现自定义委托。 所以你现在也可以这样做。

您将需要尽快将SearchViewController设置为会话委托,因此现在您将创建一个符合会话委托协议的扩展。

打开SearchViewController.swift并将// TODO 5替换为以下URLSessionDownloadDelegate扩展名:

extension SearchViewController: URLSessionDownloadDelegate {
  func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
                  didFinishDownloadingTo location: URL) {
    print("Finished downloading to \(location).")
  } 
}

唯一的非可选URLSessionDownloadDelegate方法是urlSession(_:downloadTask:didFinishDownloadingTo :),应用程序在下载完成后会调用该方法。 目前,只要下载完成,您就会打印一条消息。

1. Downloading a Track

通过所有准备工作,您现在可以将文件下载到位。 您的第一步是创建一个专用会话来处理您的下载任务。

SearchViewController.swift中,使用以下代码替换// TODO 6

lazy var downloadsSession: URLSession = {
  let configuration = URLSessionConfiguration.default
  
  return URLSession(configuration: configuration, 
                    delegate: self, 
                    delegateQueue: nil)
}()

在这里,您使用默认配置初始化单独的会话,并指定一个委托,该委托允许您通过委托调用接收URLSession事件。 这对于监视任务的进度非常有用。

将委托队列delegate queue设置为nil会导致会话创建一个串行操作队列,以执行委派方法和完成处理程序的所有调用。

请注意downloadsSession的延迟创建;这使您可以在初始化视图控制器之后延迟创建会话。 这样做允许您将self作为委托参数传递给会话初始化程序。

现在使用以下行替换viewDidLoad()末尾的// TODO 7

downloadService.downloadsSession = downloadsSession

这会将DownloadServicedownloadsSession属性设置为您刚刚定义的会话。

通过配置会话和代理,您最终可以在用户请求跟踪下载时创建下载任务。

DownloadService.swift中,使用以下实现替换startDownload(_ :)的内容:

// 1
let download = Download(track: track)
// 2
download.task = downloadsSession.downloadTask(with: track.previewURL)
// 3
download.task?.resume()
// 4
download.isDownloading = true
// 5
activeDownloads[download.track.previewURL] = download

当用户点击表格视图单元格的Download按钮时,作为TrackCellDelegateSearchViewController会识别此单元格的Track,然后使用该Track调用startDownload(_ :)

这是startDownload(_ :)中发生的事情:

  • 1) 首先使用轨道track初始化Download
  • 2) 使用新的会话对象,使用track’s preview URL创建URLSessionDownloadTask,并将其设置为Downloadtask属性。
  • 3) 您可以通过调用resume()来启动下载任务。
  • 4) 您表明下载正在进行中。
  • 5) 最后,将下载URL映射到activeDownloads中的Download

构建并运行您的应用程序,搜索任何track,然后点击单元格上的Download按钮。 过了一会儿,您将在调试控制台中看到一条消息,表示下载已完成。

Finished downloading to file:///Users/mymac/Library/Developer/CoreSimulator/Devices/74A1CE9B-7C49-46CA-9390-3B8198594088/data/Containers/Data/Application/FF0D263D-4F1D-4305-B98B-85B6F0ECFE16/tmp/CFNetworkDownload_BsbzIk.tmp.

Download按钮仍在显示,但您很快就会解决这个问题。 首先,你想要播放一些曲调!

2. Saving and Playing the Track

下载任务完成后,urlSession(_:downloadTask:didFinishDownloadingTo :)会提供临时文件位置的URL,如打印消息中所示。 您的工作是在从方法返回之前将其移动到应用程序的沙箱容器目录中的永久位置。

SearchViewController.swift中,使用以下代码替换urlSession(_:downloadTask:didFinishDownloadingTo :)中的print语句:

// 1
guard let sourceURL = downloadTask.originalRequest?.url else {
  return
}

let download = downloadService.activeDownloads[sourceURL]
downloadService.activeDownloads[sourceURL] = nil
// 2
let destinationURL = localFilePath(for: sourceURL)
print(destinationURL)
// 3
let fileManager = FileManager.default
try? fileManager.removeItem(at: destinationURL)

do {
  try fileManager.copyItem(at: location, to: destinationURL)
  download?.track.downloaded = true
} catch let error {
  print("Could not copy file to disk: \(error.localizedDescription)")
}
// 4
if let index = download?.track.index {
  DispatchQueue.main.async { [weak self] in
    self?.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], 
                               with: .none)
  }
}

以下是您在每个步骤中所做的事情:

  • 1) 您从任务中提取原始请求URL,在活动下载(active downloads)中查找相应的下载并从该字典中删除它。
  • 2) 然后,将URL传递给localFilePath(for :),它通过将URLlastPathComponent(文件的文件名和扩展名)附加到应用程序的Documents目录的路径,生成要保存的永久本地文件路径。
  • 3) 使用fileManager,将下载的文件从其临时文件位置移动到所需的目标文件路径,首先清除该位置的任何项目,然后再开始复制任务。 您还将下载track’sdownloaded属性设置为true
  • 4) 最后,使用下载track’sindex属性重新加载相应的单元格。

构建并运行项目,运行查询,然后选择任何track并下载它。 下载完成后,您将看到打印到控制台的文件路径位置:

file:///Users/mymac/Library/Developer/CoreSimulator/Devices/74A1CE9B-7C49-46CA-9390-3B8198594088/data/Containers/Data/Application/087C38CC-0CEB-4895-ADB6-F44D13C2CA5A/Documents/mzaf_2494277700123015788.plus.aac.p.m4a

Download按钮现在消失,因为委托方法将trackdownloaded属性设置为true。 点按track,您将听到它在AVPlayerViewController中播放,如下所示:


Pausing, Resuming, and Canceling Downloads

如果用户想暂停下载或完全取消该怎么办? 在本节中,您将实现暂停,恢复和取消功能,以便用户完全控制下载过程。

您将首先允许用户取消活动下载active download

1. Canceling Downloads

DownloadService.swift中,在cancelDownload(_ :)中添加以下代码:

guard let download = activeDownloads[track.previewURL] else {
  return
}

download.task?.cancel()
activeDownloads[track.previewURL] = nil

要取消下载,您将从active downloads词典中的相应Download中检索下载任务,并在其上调用cancel()以取消该任务。 然后,您将从active downloads字典中删除下载对象。

2. Pausing Downloads

您的下一个任务是让您的用户暂停下载并稍后再继续。

暂停下载与取消下载类似。 暂停取消下载任务,但也会生成恢复数据( resume data),其中包含足够的信息,以便在主机服务器支持该功能时稍后恢复下载。

使用以下代码替换pauseDownload(_ :)的内容:

guard
  let download = activeDownloads[track.previewURL],
  download.isDownloading 
  else {
    return
}

download.task?.cancel(byProducingResumeData: { data in
  download.resumeData = data
})

download.isDownloading = false

这里的关键区别是你调用cancel(byProducingResumeData :)而不是cancel()。 您为此方法提供了一个闭包参数,该参数允许您将恢复数据resume data保存到相应的Download以供将来恢复。

您还将DownloadisDownloading属性设置为false,以指示用户已暂停下载。

现在暂停功能已经完成,下一个业务顺序是允许用户恢复暂停的下载。

3. Resuming Downloads

使用以下代码替换resumeDownload(_ :)的内容:

guard let download = activeDownloads[track.previewURL] else {
  return
}

if let resumeData = download.resumeData {
  download.task = downloadsSession.downloadTask(withResumeData: resumeData)
} else {
  download.task = downloadsSession
    .downloadTask(with: download.track.previewURL)
}

download.task?.resume()
download.isDownloading = true

当用户恢复下载时,请检查相应的Download是否存在恢复数据resume data。 如果找到,您将通过使用恢复数据调用downloadTask(withResumeData :)来创建新的下载任务。 如果由于任何原因缺少恢复数据,您将使用下载URL创建新的下载任务。

在任何一种情况下,您都将通过调用resume启动任务,并将DownloadisDownloading标志设置为true以指示下载已恢复。


Showing and Hiding the Pause/Resume and Cancel Buttons

这三个功能只需要执行一项操作:您需要根据需要显示或隐藏Pause/Resume and Cancel按钮。

要做到这一点,TrackCellconfigure(track:downloaded:)需要知道该track是否有active download以及它是否正在下载。

TrackCell.swift中,将configure(track:downloaded :)更改为configure(track:downloaded:download :)

func configure(track: Track, downloaded: Bool, download: Download?) {

SearchViewController.swift中,调用tableView(_:cellForRowAt:)

cell.configure(track: track,
               downloaded: track.downloaded,
               download: downloadService.activeDownloads[track.previewURL])

在这里,您从activeDownloads中提取track的下载对象。

回到TrackCell.swift,在configure(track:downloaded:download :)中找到// TODO 14并添加以下属性:

var showDownloadControls = false

然后使用以下内容替换// TODO 15

if let download = download {
  showDownloadControls = true
  let title = download.isDownloading ? "Pause" : "Resume"
  pauseButton.setTitle(title, for: .normal)
}

正如注释所述,非nil下载对象意味着下载正在进行中,因此单元格应显示下载控件:Pause/Resume and Cancel。 由于暂停和恢复功能共享相同的按钮,您将根据需要在两种状态之间切换按钮。

在这个if-closure下面,添加以下代码:

pauseButton.isHidden = !showDownloadControls
cancelButton.isHidden = !showDownloadControls

在此处,仅在下载处于活动状态时才显示单元格的按钮。

最后,替换此方法的最后一行:

downloadButton.isHidden = downloaded

使用以下代码:

downloadButton.isHidden = downloaded || showDownloadControls

在这里,如果正在下载曲目track,则告诉单元格隐藏Download按钮。

构建并运行您的项目。 同时下载几首曲目,您将能够随意暂停,恢复和取消它们:


Showing Download Progress

此时,应用程序正常运行,但未显示下载进度。 要改善用户体验,您需要更改应用以侦听下载进度事件并在单元格中显示进度。 有一个会话委托方法,非常适合这项工作!

首先,在TrackCell.swift中,使用以下辅助方法替换// TODO 16

func updateDisplay(progress: Float, totalSize : String) {
  progressView.progress = progress
  progressLabel.text = String(format: "%.1f%% of %@", progress * 100, totalSize)
}

track cell具有progressViewprogressLabel outlets。 委托方法将调用此帮助程序方法来设置其值。

接下来,在SearchViewController.swift中,将以下委托方法添加到URLSessionDownloadDelegate扩展:

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
                  didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
                  totalBytesExpectedToWrite: Int64) {
  // 1
  guard
    let url = downloadTask.originalRequest?.url,
    let download = downloadService.activeDownloads[url]  
    else {
      return
  }
  // 2
  download.progress = 
    Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
  // 3
  let totalSize = 
    ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, 
                              countStyle: .file) 
  // 4
  DispatchQueue.main.async {
    if let trackCell = 
      self.tableView.cellForRow(at: IndexPath(row: download.track.index,
                                              section: 0)) as? TrackCell {
      trackCell.updateDisplay(progress: download.progress, 
                              totalSize: totalSize)
    }
  }
}

仔细研究这种委托方法,一步一步:

  • 1) 您提取所提供的downloadTaskURL,并使用它在您的活动下载字典中查找匹配的Download
  • 2) 该方法还提供了您编写的总字节数以及您希望写入的总字节数。您将进度计算为这两个值的比率,并将结果保存在Download中。track单元格将使用此值更新进度视图。
  • 3) ByteCountFormatter获取一个字节值并生成一个人类可读的字符串,显示总下载文件大小。您将使用此字符串显示下载大小以及完成百分比。
  • 4) 最后,您找到负责显示Track的单元格,并调用单元格的辅助方法以使用从前面步骤派生的值更新其progress view and progress label。这涉及到UI,因此您可以在主队列中执行此操作。

Displaying the Download’s Progress

现在,更新单元的配置以在下载进行时显示进度视图和状态。

打开TrackCell.swift。在configure(track:downloaded:download :)中,在设置了pause按钮标题后,在if-closure中添加以下行:

progressLabel.text = download.isDownloading ? "Downloading..." : "Paused"

这使得单元格在委托方法第一次更新之前显示,并且下载暂停时显示。

现在,在if-closure下面添加以下代码,在两个按钮的isHidden行下面:

progressView.isHidden = !showDownloadControls
progressLabel.isHidden = !showDownloadControls

这仅在下载过程中显示progress view and label

构建并运行您的项目。 下载任何track,随着下载的进行,您应该会看到进度条状态更新:


Enabling Background Transfers

此时您的应用程序非常实用,但还有一个主要的增强功能:后台传输(Background transfers)

在此模式下,即使您的应用在后台或因任何原因崩溃,下载也会继续。这对于非常小的歌曲片段来说并不是必需的,但如果您的应用传输大型文件,您的用户将会喜欢此功能。

但是,如果您的应用没有运行,这怎么做到的?

操作系统OS在应用程序外部运行一个单独的守护程序来管理后台传输任务,并在下载任务运行时将适当的委托消息发送到应用程序。如果应用程序在主动传输期间终止,则任务将在后台继续运行,不受影响。

任务完成后,守护程序(daemon)将在后台重新启动应用程序。重新启动的应用程序将重新创建后台会话以接收相关的完成委托消息并执行任何所需的操作,例如将下载的文件保存到磁盘。

注意:如果用户通过从应用切换器强制退出来终止应用,系统将取消所有会话的后台传输,并且不会尝试重新启动应用。

您可以通过使用后台background会话配置创建会话来访问此魔法。

SearchViewController.swift中,在downloadsSession的初始化中,找到以下代码行:

let configuration = URLSessionConfiguration.default

替换为下面代码

let configuration = 
  URLSessionConfiguration.background(withIdentifier:
                                       "com.xxxx.HalfTunes.bgSession")

您将使用特殊的后台background会话配置,而不是使用默认default会话配置。 请注意,您还为会话设置了唯一标识符,以便您的应用在需要时创建新的后台会话。

注意:您不能为后台配置创建多个会话,因为系统使用配置的标识符将任务与会话相关联。


Relaunching Your App

如果后台任务在应用程序未运行时完成,则应用程序将在后台重新启动。 您需要从应用代理处理此事件。

切换到AppDelegate.swift,用以下代码替换// TODO 17

var backgroundSessionCompletionHandler: (() -> Void)?

// TODO 18替换为下面

func application(
  _ application: UIApplication,
  handleEventsForBackgroundURLSession 
    handleEventsForBackgroundURLSessionidentifier: String,
  completionHandler: @escaping () -> Void) {
    backgroundSessionCompletionHandler = completionHandler
}

在这里,您将提供的completionHandler保存为app delegate中的变量供以后使用。

application(_:handleEventsForBackgroundURLSession :)唤醒应用程序来处理已完成的后台任务。您需要在此方法中处理两个项目:

  • 首先,应用程序需要使用此委托方法提供的标识符重新创建适当的后台配置和会话。但是由于这个应用程序在实例化SearchViewController时会创建后台会话,所以此时您已经重新连接了!
  • 其次,您需要捕获此委托方法提供的完成处理程序。调用完成处理程序告诉操作系统您的应用程序已完成当前会话的所有后台活动。它还会使操作系统对更新的UI进行快照,以便在应用切换器中显示。

调用提供的完成处理completion handler程序的地方是urlSessionDidFinishEvents(forBackgroundURLSession :),这是一个URLSessionDelegate方法,当后台会话上的所有任务都完成时触发。

SearchViewController.swift中,使用以下扩展名替换// TODO 19

extension SearchViewController: URLSessionDelegate {
  func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
      if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
        let completionHandler = appDelegate.backgroundSessionCompletionHandler {
        appDelegate.backgroundSessionCompletionHandler = nil
        
        completionHandler()
      }
    }
  } 
}

上面的代码从app delegate中获取存储的完成处理程序completion handler,并在主线程上调用它。 您可以通过获取UIApplication的共享实例找到app delegate,这可以通过UIKit import进行获取。


Testing Your App’s Functionality

构建并运行您的应用程序。 开始几个并发下载,然后点击Home按钮将应用程序发送到后台。 等到您认为下载已完成,然后双击Home按钮以显示应用程序切换器。

下载应该已经完成,您应该在应用程序快照中看到它们的新状态。 打开应用程序以确认:

你现在有一个功能音乐流媒体应用程序!

如果您想进一步探索该主题,那么URLSession主题将多于本教程中的主题。 例如,您还可以尝试上载任务和会话配置设置,例如超时值和缓存策略。

要了解有关这些功能(以及其他功能!)的更多信息,请查看以下资源:

  • Apple的包含有关您想要做的所有事情的全面信息。
  • 是一个受欢迎的第三方iOS网络库。

后记

本篇主要讲述了一种和网络请求有关的类NSURLSession,感兴趣的给个赞或者关注~~~

Top