版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.06.15 星期六 |
前言
开始
首先看下写作环境
Swift 5, iOS 12, Xcode 10
在此URLSession
教程中,您将学习如何创建HTTP
请求以及实现可以暂停和恢复的后台下载。
无论应用程序从服务器检索应用程序数据,更新您的社交媒体状态还是将远程文件下载到磁盘,网络请求都是让奇迹发生的原因。 为了帮助您满足网络请求的许多要求,Apple提供了URLSession
,这是一个用于上传和下载内容的完整网络API。
打开下载好的入门项目,它包含用于搜索歌曲和显示搜索结果的用户界面,具有一些存根功能的网络类以及用于存储和播放曲目的辅助方法。 这使您可以专注于实现应用程序的网络方面。
构建并运行项目。 您将在顶部看到一个带有搜索栏的视图,下面是一个空表视图:
在搜索栏中键入查询,然后点按Search
。 视图仍然是空的。 不过不用担心,您将使用新的URLSession
调用更改此情况。
URLSession Overview
在开始之前,了解URLSession
及其组成类非常重要,因此请查看下面的快速概述。
URLSession
既是一类,也是一套用于处理基于HTTP
和HTTPS
的请求的类:
URLSession
是负责发送和接收请求的关键对象。 您可以通过URLSessionConfiguration
创建它,它有三种形式:
- default:创建使用磁盘持久全局缓存,凭据和cookie存储对象的默认配置对象。
-
ephemeral:与
default
配置类似,不同之处在于您将所有与会话相关的数据存储在内存中。 将此视为“私人”会话。 - background:允许会话在后台执行上载或下载任务。 即使应用程序本身被系统暂停或终止,传输仍会继续。
URLSessionTask
是一个表示任务对象的抽象类。 会话创建一个或多个任务来执行获取数据和下载或上载文件的实际工作。
1. Understanding Session Task Types
有三种类型的具体会话任务:
-
URLSessionDataTask:将此任务用于
GET
请求,以将数据从服务器检索到内存。 -
URLSessionUploadTask:使用此任务通过
POST
或PUT
方法将文件从磁盘上载到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)
urlComponents
的url
属性是可选的,因此您将其解包为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:下载
track
的URLSessionDownloadTask
。 -
track:要下载的曲目。
track
的url
属性也充当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
这会将DownloadService
的downloadsSession
属性设置为您刚刚定义的会话。
通过配置会话和代理,您最终可以在用户请求跟踪下载时创建下载任务。
在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
按钮时,作为TrackCellDelegate
的SearchViewController
会识别此单元格的Track
,然后使用该Track
调用startDownload(_ :)
。
这是startDownload(_ :)
中发生的事情:
- 1) 首先使用轨道
track
初始化Download
。 - 2) 使用新的会话对象,使用
track’s preview URL
创建URLSessionDownloadTask
,并将其设置为Download
的task
属性。 - 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 :)
,它通过将URL
的lastPathComponent
(文件的文件名和扩展名)附加到应用程序的Documents
目录的路径,生成要保存的永久本地文件路径。 - 3) 使用
fileManager
,将下载的文件从其临时文件位置移动到所需的目标文件路径,首先清除该位置的任何项目,然后再开始复制任务。 您还将下载track’s
的downloaded
属性设置为true
。 - 4) 最后,使用下载
track’s
的index
属性重新加载相应的单元格。
构建并运行项目,运行查询,然后选择任何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
按钮现在消失,因为委托方法将track
的downloaded
属性设置为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
以供将来恢复。
您还将Download
的isDownloading
属性设置为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
启动任务,并将Download
的isDownloading
标志设置为true
以指示下载已恢复。
Showing and Hiding the Pause/Resume and Cancel Buttons
这三个功能只需要执行一项操作:您需要根据需要显示或隐藏Pause/Resume and Cancel
按钮。
要做到这一点,TrackCell
的configure(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
具有progressView
和progressLabel
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) 您提取所提供的
downloadTask
的URL
,并使用它在您的活动下载字典中查找匹配的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
,感兴趣的给个赞或者关注~~~