出错后如何继续 URLSession dataTaskPublisher 或其他 Publisher?

How can I continue URLSession dataTaskPublisher or another Publisher after error?

我有一个应用程序需要检查服务器上的状态:

我通过合并两个发布者,然后调用 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()
}