Har*_*lue 8 ios async-await swift urlsession
我有一个网络层,当前使用完成处理程序来传递操作完成的结果。
由于我支持多个 iOS 版本,因此我在应用程序内扩展网络层以提供对合并的支持。我想将其扩展到现在也支持异步/等待,但我很难理解如何以允许我取消请求的方式实现这一点。
基本实现如下所示;
protocol HTTPClientTask {
func cancel()
}
protocol HTTPClient {
typealias Result = Swift.Result<(data: Data, response: HTTPURLResponse), Error>
@discardableResult
func dispatch(_ request: URLRequest, completion: @escaping (Result) -> Void) -> HTTPClientTask
}
final class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func dispatch(_ request: URLRequest, completion: @escaping (HTTPClient.Result) -> Void) -> HTTPClientTask {
let task = session.dataTask(with: request) { data, response, error in
completion(Result {
if let error = error {
throw error
} else if let data = data, let response = response as? HTTPURLResponse {
return (data, response)
} else {
throw UnexpectedValuesRepresentation()
}
})
}
task.resume()
return URLSessionTaskWrapper(wrapped: task)
}
}
private extension URLSessionHTTPClient {
struct UnexpectedValuesRepresentation: Error {}
struct URLSessionTaskWrapper: HTTPClientTask {
let wrapped: URLSessionTask
func cancel() {
wrapped.cancel()
}
}
}
Run Code Online (Sandbox Code Playgroud)
它非常简单地提供了一个抽象,允许我注入一个URLSession实例。
通过返回,HTTPClientTask我可以cancel从客户端调用并结束请求。
Combine我使用如下方式在客户端应用程序中扩展它:
extension HTTPClient {
typealias Publisher = AnyPublisher<(data: Data, response: HTTPURLResponse), Error>
func dispatchPublisher(for request: URLRequest) -> Publisher {
var task: HTTPClientTask?
return Deferred {
Future { completion in
task = self.dispatch(request, completion: completion)
}
}
.handleEvents(receiveCancel: { task?.cancel() })
.eraseToAnyPublisher()
}
}
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,我现在有一个支持取消任务的界面。
但是async/await,我不确定这应该是什么样子,我如何提供取消请求的机制。
我目前的尝试是;
extension HTTPClient {
func dispatch(_ request: URLRequest) async -> HTTPClient.Result {
let task = Task { () -> (data: Data, response: HTTPURLResponse) in
return try await withCheckedThrowingContinuation { continuation in
self.dispatch(request) { result in
switch result {
case let .success(values): continuation.resume(returning: values)
case let .failure(error): continuation.resume(throwing: error)
}
}
}
}
do {
let output = try await task.value
return .success(output)
} catch {
return .failure(error)
}
}
}
Run Code Online (Sandbox Code Playgroud)
然而,这只是提供了async实现,我无法取消它。
这应该如何处理?
Rob*_*Rob 12
Swift\xe2\x80\x99 的新并发模型可以很好地处理取消。虽然 WWDC 2021 视频重点介绍了checkCancellation和isCancelled模式(例如,探索 Swift视频中的结构化并发),但在这种情况下,人们将使用withTaskCancellationHandler创建一个任务,在任务本身被取消时取消网络请求。(显然,这只是 iOS 13/14 中的一个问题,因为在 iOS 15 中人们只需使用提供的async方法,data(for:delegate)或者data(from:delegate:),它也可以很好地处理取消。)
例如,请参阅SE-0300:将异步任务与同步代码连接起来的延续:其他示例。这个download例子有点过时了,所以这里是一个更新的版本:
extension URLSession {\n @available(iOS, deprecated: 15, message: "Use `data(from:delegate:)` instead")\n @available(macOS, deprecated: 12, message: "Use `data(from:delegate:)` instead")\n func data(with url: URL) async throws -> (URL, URLResponse) {\n try await download(with: URLRequest(url: url))\n }\n\n @available(iOS, deprecated: 15, message: "Use `data(for:delegate:)` instead")\n @available(macOS, deprecated: 12, message: "Use `data(for:delegate:)` instead")\n func data(with request: URLRequest) async throws -> (Data, URLResponse) {\n let sessionTask = SessionTask(session: self)\n\n return try await withTaskCancellationHandler {\n try await withCheckedThrowingContinuation { continuation in\n Task {\n await sessionTask.data(for: request) { data, response, error in\n guard let data, let response else {\n continuation.resume(throwing: error ?? URLError(.badServerResponse))\n return\n }\n\n continuation.resume(returning: (data, response))\n }\n }\n }\n } onCancel: {\n Task { await sessionTask.cancel() }\n }\n }\n}\n\nprivate extension URLSession {\n actor SessionTask {\n var state: State = .ready\n private let session: URLSession\n\n init(session: URLSession) {\n self.session = session\n }\n\n func cancel() {\n if case .executing(let task) = state {\n task.cancel()\n }\n state = .cancelled\n }\n }\n}\n\n// MARK: Data\n\nextension URLSession.SessionTask {\n func data(for request: URLRequest, completionHandler: @Sendable @escaping (Data?, URLResponse?, Error?) -> Void) {\n if case .cancelled = state {\n completionHandler(nil, nil, CancellationError())\n return\n }\n\n let task = session.dataTask(with: request, completionHandler: completionHandler)\n\n state = .executing(task)\n task.resume()\n }\n}\n\nextension URLSession.SessionTask {\n enum State {\n case ready\n case executing(URLSessionTask)\n case cancelled\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n对我的代码片段的一些小观察:
\n我给出这些名称是为了避免与 iOS 15 方法名称冲突,但添加了deprecated消息以通知开发人员在放弃 iOS 13/14 支持后使用 iOS 15 演绎版。
data(from:delegate:)我偏离了 SE-0300\xe2\x80\x99s 示例,遵循和方法的模式data(for:delegate:)(使用Data和 a返回元组URLResponse)。
需要来同步对 的访问(原始示例actor中没有)。URLSessionTask
请注意,根据SE-0304,关于withTaskCancellationHandler:
\n\n如果任务在调用时已被取消
\nwithTaskCancellationHandler,则在执行操作块之前立即调用取消处理程序。
因此,actor上面使用一个state变量来确定请求是否已经被取消,并且立即恢复,CancellationError如果已经取消则抛出 a 。
但所有这些都与当前的问题无关。简而言之,使用withTaskCancellationHandler.
例如,以下是我在任务组中启动的五个图像请求,由Charles监控:
\n\n这里是相同的请求,但这次我取消了整个任务组(并且取消成功地为我停止了相关的网络请求):
\n\n(显然x轴刻度是不同的。)
\n如果您需要下载演绎版(以换行downloadTask),您可以补充上述内容:
extension URLSession {\n @available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")\n @available(macOS, deprecated: 12, message: "Use `download(from:delegate:)` instead")\n func download(with url: URL) async throws -> (URL, URLResponse) {\n try await download(with: URLRequest(url: url))\n }\n\n @available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")\n @available(macOS, deprecated: 12, message: "Use `download(for:delegate:)` instead")\n func download(with request: URLRequest) async throws -> (URL, URLResponse) {\n let sessionTask = SessionTask(session: self)\n\n return try await withTaskCancellationHandler {\n try await withCheckedThrowingContinuation { continuation in\n Task {\n await sessionTask.download(for: request) { location, response, error in\n guard let location, let response else {\n continuation.resume(throwing: error ?? URLError(.badServerResponse))\n return\n }\n\n // since continuation can happen later, let\xe2\x80\x99s figure out where to store it ...\n\n let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())\n .appendingPathComponent(UUID().uuidString)\n .appendingPathExtension(request.url!.pathExtension)\n\n // ... and move it to there\n\n do {\n try FileManager.default.moveItem(at: location, to: tempURL)\n } catch {\n continuation.resume(throwing: error)\n return\n }\n\n continuation.resume(returning: (tempURL, response))\n }\n }\n }\n } onCancel: {\n Task { await sessionTask.cancel() }\n }\n }\n}\n\nextension URLSession.SessionTask {\n func download(for request: URLRequest, completionHandler: @Sendable @escaping (URL?, URLResponse?, Error?) -> Void) {\n if case .cancelled = state {\n completionHandler(nil, nil, CancellationError())\n return\n }\n\n let task = session.downloadTask(with: request, completionHandler: completionHandler)\n\n state = .executing(task)\n task.resume()\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
8032 次 |
| 最近记录: |