多年来,Foundation内置的URLSession API已经成长为一个多功能的、非常强大的网络工具,以至于通常不再需要第三方库来以简单直接的方式执行标准的HTTP网络调用。
虽然URLSession 带来的许多便利API都集中在用于获取数据的GET 请求上,但在这篇文章中,让我们看看其他的HTTP方法也可以被使用--特别是如何在没有任何外部依赖的情况下执行不同种类的POST 请求。
数据和上传任务
也许使用URLSession 来执行POST 请求的最简单的方法是使用其各种dataTask API(支持委托和基于闭合的回调,以及Combine)的基于URLRequest 的重载。在许多其他方面,URLRequest ,使我们能够定制一个给定的网络调用应该使用什么httpMethod ,以及其他有用的参数,如发送什么httpBody 数据,以及使用什么cachePolicy 。这里有一个例子:
struct Networking {
var urlSession = URLSession.shared
func sendPostRequest(
to url: URL,
body: Data,
then handler: @escaping (Result<Data, Error>) -> Void
) {
// To ensure that our request is always sent, we tell
// the system to ignore all local cache data:
var request = URLRequest(
url: url,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
request.httpBody = body
let task = urlSession.dataTask(
with: request,
completionHandler: { data, response, error in
// Validate response and call handler
...
}
)
task.resume()
}
}
根据我们的POST 请求被发送到的服务器,我们可能还想进一步配置我们的URLRequest 实例,例如,给它一个Content-Type 头。
另外,我们可以选择使用uploadTask API来创建我们的请求任务,这既可以让我们在应用程序处于后台的时候上传数据,又可以提供内置的支持,将主体数据直接附加到任务本身。
struct Networking {
var urlSession = URLSession.shared
func sendPostRequest(
to url: URL,
body: Data,
then handler: @escaping (Result<Data, Error>) -> Void
) {
var request = URLRequest(
url: url,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
let task = urlSession.uploadTask(
with: request,
from: body,
completionHandler: { data, response, error in
// Validate response and call handler
...
}
)
task.resume()
}
}
观察进度更新
虽然上述两种方法中的任何一种在发送较小的数据作为POST 请求的一部分时都能完全正常工作,但有时我们可能想上传一个可能相当大的文件(即使是简单的东西,如图片,也可能是几兆字节大小)。在这样做的时候,我们可能想给用户提供某种形式的实时进度更新,因为否则我们的应用程序的用户界面可能会显得很慢甚至没有反应。
不幸的是,没有一个基于闭合的或由Combine驱动的URLSession API为观察请求的持续进展提供直接支持,但幸运的是,我们可以使用良好的老式委托模式很容易地实现这一点。
为了证明这一点,让我们创建一个FileUploader 类(它需要是Objective-C的NSObject 的子类)。然后我们将使用一个自定义的 URLSession 实例,而不是shared ,因为这将使我们成为这个会话的委托人。然后,我们将定义一个API,让我们从一个给定的本地URL上传文件,我们将让API的调用者传递两个闭包--一个用于处理进度事件,以及一个标准的完成处理。最后,我们将根据每个上传任务的ID将所有进度事件处理程序存储在一个字典中,这样我们以后就可以在我们的委托协议实现中调用这些闭包。
class FileUploader: NSObject {
// We'll define a few type aliases to make our code easier to read:
typealias Percentage = Double
typealias ProgressHandler = (Percentage) -> Void
typealias CompletionHandler = (Result<Void, Error>) -> Void
// Creating our custom URLSession instance. We'll do it lazily
// to enable 'self' to be passed as the session's delegate:
private lazy var urlSession = URLSession(
configuration: .default,
delegate: self,
delegateQueue: .main
)
private var progressHandlersByTaskID = [Int : ProgressHandler]()
func uploadFile(
at fileURL: URL,
to targetURL: URL,
progressHandler: @escaping ProgressHandler,
completionHandler: @escaping CompletionHandler
) {
var request = URLRequest(
url: targetURL,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
let task = urlSession.uploadTask(
with: request,
fromFile: fileURL,
completionHandler: { data, response, error in
// Validate response and call handler
...
}
)
progressHandlersByTaskID[task.taskIdentifier] = progressHandler
task.resume()
}
}
接下来,让我们实现URLSessionTaskDelegate 协议,它是基础URLSessionDelegate 协议的一个专门版本,增加了一些额外的方法,使我们能够观察特定任务的事件。在这种情况下,我们只想在一个给定的URLSessionTask 的进度被更新时得到通知,这可以通过实现以下方法来实现:
extension FileUploader: URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64
) {
let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
let handler = progressHandlersByTaskID[task.taskIdentifier]
handler?(progress)
}
}
有了上述方法,我们现在就可以使用传递到每个progressHandler 闭包中的百分比值来驱动任何我们想用来可视化上传进度的UI组件--例如ProgressView 、UIProgressView 或NSProgressIndicator 。
一个随时间变化的进度流
最后,让我们也来看看我们如何将上述FileUploader ,以使用Combine而不是多个闭包。毕竟,Combine以 "随时间变化的值 "为中心的设计非常适用于进度更新的建模,因为我们希望随时间变化发送一些百分比的值,然后以一个完成事件结束,这正是Combine发布器的作用。
虽然我们可以选择使用自定义发布器来实现这一功能,但在这种情况下,让我们使用CurrentValueSubject ,它提供了一种内置的方式来发送数值,然后被缓存并发送给每个新的订阅者。这样,我们可以将每个上传任务与一个给定的主题相关联(就像我们之前存储每个progressHandler 关闭一样),然后使用eraseToAnyPublisher API将该主题作为一个发布者返回--像这样:
class FileUploader: NSObject {
typealias Percentage = Double
typealias Publisher = AnyPublisher<Percentage, Error>
private typealias Subject = CurrentValueSubject<Percentage, Error>
private lazy var urlSession = URLSession(
configuration: .default,
delegate: self,
delegateQueue: .main
)
private var subjectsByTaskID = [Int : Subject]()
func uploadFile(at fileURL: URL,
to targetURL: URL) -> Publisher {
var request = URLRequest(
url: targetURL,
cachePolicy: .reloadIgnoringLocalCacheData
)
request.httpMethod = "POST"
let subject = Subject(0)
var removeSubject: (() -> Void)?
let task = urlSession.uploadTask(
with: request,
fromFile: fileURL,
completionHandler: { data, response, error in
// Validate response and send completion
...
subject.send(completion: .finished)
removeSubject?()
}
)
subjectsByTaskID[task.taskIdentifier] = subject
removeSubject = { [weak self] in
self?.subjectsByTaskID.removeValue(forKey: task.taskIdentifier)
}
task.resume()
return subject.eraseToAnyPublisher()
}
}
现在剩下的就是更新我们的URLSessionTaskDelegate 实现,将每个进度值发送到与有关任务相关的主题,而不是调用一个闭包。
extension FileUploader: URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64
) {
let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
let subject = subjectsByTaskID[task.taskIdentifier]
subject?.send(progress)
}
}
就这样,我们现在可以轻松地执行更简单的POST 请求和文件上传,并使用Combine或基于closure的API来处理进度事件。真的很好!
总结
虽然以上一系列的实现并不是一个完整的网络库,但它们希望能证明URLSession 所提供的内置功能往往是我们执行许多不同类型的请求所需要的,包括那些涉及发布数据或上传文件的请求。
我希望你觉得这篇文章很有用。