出错后如何继续 URLSession dataTaskPublisher 或其他 Publisher?
How can I continue URLSession dataTaskPublisher or another Publisher after error?
我有一个应用程序需要检查服务器上的状态:
- 每 30 秒
- 每当应用程序进入前台时
我通过合并两个发布者,然后调用 flatMap
合并发布者的输出来触发 API 请求。
我有一个函数可以发出 API 请求和 returns 结果的发布者,还包括检查响应并根据其内容抛出错误的逻辑。
似乎一旦抛出 StatusError.statusUnavailable
错误,statusSubject
就会停止获取更新。如何更改此行为,以便 statusSubject
在错误发生后继续获取更新?我希望 API 请求每 30 秒以及在应用程序打开时继续,即使出现错误。
我还有一些其他地方让我对我当前的代码感到困惑,如评论所示,因此我也非常感谢在这些方面的任何帮助、解释或想法。
这是我的示例代码:
import Foundation
import SwiftUI
import Combine
struct StatusResponse: Codable {
var response: String?
var error: String?
}
enum StatusError: Error {
case statusUnavailable
}
class Requester {
let statusSubject = CurrentValueSubject<StatusResponse,Error>(StatusResponse(response: nil, error: nil))
private var cancellables: [AnyCancellable] = []
init() {
// Check for updated status every 30 seconds
let timer = Timer
.publish(every: 30,
tolerance: 10,
on: .main,
in: .common,
options: nil)
.autoconnect()
.map { _ in true } // how else should I do this to be able to get these two publisher outputs to match so I can merge them?
// also check status on server when the app comes to the foreground
let foreground = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
// bring the two publishes together
let timerForegroundCombo = timer.merge(with: foreground)
timerForegroundCombo
// I don't understand why this next line is necessary, but the compiler gives an error if I don't have it
.setFailureType(to: Error.self)
.flatMap { _ in self.apiRequest() }
.subscribe(statusSubject)
.store(in: &cancellables)
}
private func apiRequest() -> AnyPublisher<StatusResponse, Error> {
let url = URL(string: "http://www.example.com/status-endpoint")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return URLSession.shared.dataTaskPublisher(for: request)
.mapError { [=10=] as Error }
.map { [=10=].data }
.decode(type: StatusResponse.self, decoder: JSONDecoder())
.tryMap({ status in
if let error = status.error,
error.contains("status unavailable") {
throw StatusError.statusUnavailable
} else {
return status
}
})
.eraseToAnyPublisher()
}
}
您可以使用 retry() 来获取或捕获此类行为...更多信息请点击此处:
https://www.avanderlee.com/swift/combine-error-handling/
发布失败总是会结束订阅。由于您想在出错后继续发布,因此您不能将您的错误发布为失败。您必须改为更改发布者的输出类型。标准库提供了Result
,这就是你应该使用的。
func makeStatusPublisher() -> AnyPublisher<Result<StatusResponse, Error>, Never> {
let timer = Timer
.publish(every: 30, tolerance: 10, on: .main, in: .common)
.autoconnect()
.map { _ in true } // This is the correct way to merge with the notification publisher.
let notes = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
return timer.merge(with: notes)
.flatMap({ _ in
statusResponsePublisher()
.map { Result.success([=10=]) }
.catch { Just(Result.failure([=10=])) }
})
.eraseToAnyPublisher()
}
此发布者定期发出 .success(response)
或 .failure(error)
,并且永远不会失败。
但是,您应该问问自己,如果用户反复切换应用程序会怎样?或者,如果 API 请求需要超过 30 秒才能完成怎么办? (或两者都有?)您将同时收到多个请求 运行,响应将按照它们到达的顺序处理,这可能不是请求的发送顺序。
解决此问题的一种方法是使用 flatMap(maxPublisher: .max(1)) { ... }
,这使得 flatMap
在收到未完成的请求时忽略计时器和通知信号。但它可能对每个信号启动一个新请求并取消先前的请求会更好。将 flatMap
更改为 map
,然后为该行为更改 switchToLatest
:
func makeStatusPublisher2() -> AnyPublisher<Result<StatusResponse, Error>, Never> {
let timer = Timer
.publish(every: 30, tolerance: 10, on: .main, in: .common)
.autoconnect()
.map { _ in true } // This is the correct way to merge with the notification publisher.
let notes = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
return timer.merge(with: notes)
.map({ _ in
statusResponsePublisher()
.map { Result<StatusResponse, Error>.success([=11=]) }
.catch { Just(Result<StatusResponse, Error>.failure([=11=])) }
})
.switchToLatest()
.eraseToAnyPublisher()
}
我有一个应用程序需要检查服务器上的状态:
- 每 30 秒
- 每当应用程序进入前台时
我通过合并两个发布者,然后调用 flatMap
合并发布者的输出来触发 API 请求。
我有一个函数可以发出 API 请求和 returns 结果的发布者,还包括检查响应并根据其内容抛出错误的逻辑。
似乎一旦抛出 StatusError.statusUnavailable
错误,statusSubject
就会停止获取更新。如何更改此行为,以便 statusSubject
在错误发生后继续获取更新?我希望 API 请求每 30 秒以及在应用程序打开时继续,即使出现错误。
我还有一些其他地方让我对我当前的代码感到困惑,如评论所示,因此我也非常感谢在这些方面的任何帮助、解释或想法。
这是我的示例代码:
import Foundation
import SwiftUI
import Combine
struct StatusResponse: Codable {
var response: String?
var error: String?
}
enum StatusError: Error {
case statusUnavailable
}
class Requester {
let statusSubject = CurrentValueSubject<StatusResponse,Error>(StatusResponse(response: nil, error: nil))
private var cancellables: [AnyCancellable] = []
init() {
// Check for updated status every 30 seconds
let timer = Timer
.publish(every: 30,
tolerance: 10,
on: .main,
in: .common,
options: nil)
.autoconnect()
.map { _ in true } // how else should I do this to be able to get these two publisher outputs to match so I can merge them?
// also check status on server when the app comes to the foreground
let foreground = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
// bring the two publishes together
let timerForegroundCombo = timer.merge(with: foreground)
timerForegroundCombo
// I don't understand why this next line is necessary, but the compiler gives an error if I don't have it
.setFailureType(to: Error.self)
.flatMap { _ in self.apiRequest() }
.subscribe(statusSubject)
.store(in: &cancellables)
}
private func apiRequest() -> AnyPublisher<StatusResponse, Error> {
let url = URL(string: "http://www.example.com/status-endpoint")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return URLSession.shared.dataTaskPublisher(for: request)
.mapError { [=10=] as Error }
.map { [=10=].data }
.decode(type: StatusResponse.self, decoder: JSONDecoder())
.tryMap({ status in
if let error = status.error,
error.contains("status unavailable") {
throw StatusError.statusUnavailable
} else {
return status
}
})
.eraseToAnyPublisher()
}
}
您可以使用 retry() 来获取或捕获此类行为...更多信息请点击此处: https://www.avanderlee.com/swift/combine-error-handling/
发布失败总是会结束订阅。由于您想在出错后继续发布,因此您不能将您的错误发布为失败。您必须改为更改发布者的输出类型。标准库提供了Result
,这就是你应该使用的。
func makeStatusPublisher() -> AnyPublisher<Result<StatusResponse, Error>, Never> {
let timer = Timer
.publish(every: 30, tolerance: 10, on: .main, in: .common)
.autoconnect()
.map { _ in true } // This is the correct way to merge with the notification publisher.
let notes = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
return timer.merge(with: notes)
.flatMap({ _ in
statusResponsePublisher()
.map { Result.success([=10=]) }
.catch { Just(Result.failure([=10=])) }
})
.eraseToAnyPublisher()
}
此发布者定期发出 .success(response)
或 .failure(error)
,并且永远不会失败。
但是,您应该问问自己,如果用户反复切换应用程序会怎样?或者,如果 API 请求需要超过 30 秒才能完成怎么办? (或两者都有?)您将同时收到多个请求 运行,响应将按照它们到达的顺序处理,这可能不是请求的发送顺序。
解决此问题的一种方法是使用 flatMap(maxPublisher: .max(1)) { ... }
,这使得 flatMap
在收到未完成的请求时忽略计时器和通知信号。但它可能对每个信号启动一个新请求并取消先前的请求会更好。将 flatMap
更改为 map
,然后为该行为更改 switchToLatest
:
func makeStatusPublisher2() -> AnyPublisher<Result<StatusResponse, Error>, Never> {
let timer = Timer
.publish(every: 30, tolerance: 10, on: .main, in: .common)
.autoconnect()
.map { _ in true } // This is the correct way to merge with the notification publisher.
let notes = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
return timer.merge(with: notes)
.map({ _ in
statusResponsePublisher()
.map { Result<StatusResponse, Error>.success([=11=]) }
.catch { Just(Result<StatusResponse, Error>.failure([=11=])) }
})
.switchToLatest()
.eraseToAnyPublisher()
}