错误后如何继续URLSession dataTaskPublisher或其他Publisher? [英] How can I continue URLSession dataTaskPublisher or another Publisher after error?

查看:93
本文介绍了错误后如何继续URLSession dataTaskPublisher或其他Publisher?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have an app that needs to check a status on a server:

  • 每30秒
  • 每当应用进入前台时

我这样做是通过合并两个发布者,然后调用合并的发布者的输出 flatMap 来触发API请求.

I'm doing this by merging two publishers, then calling flatMap the merged publisher's output to trigger the API request.

我有一个发出API请求并返回结果发布者的函数,还包括检查响应并根据其内容引发错误的逻辑.

I have a function that makes an API request and returns a publisher of the result, also including logic to check the response and throw an error depending on its contents.

似乎一旦抛出 StatusError.statusUnavailable 错误, statusSubject 就会停止获取更新.如何更改此行为,以便 statusSubject 在错误发生后继续获取更新?我希望API请求每隔30秒和打开应用程序时继续一次,即使出现错误也是如此.

It seems that once a StatusError.statusUnavailable error is thrown, the statusSubject stops getting updates. How can I change this behavior so the statusSubject continues getting updates after the error? I want the API requests to continue every 30 seconds and when the app is opened, even after there is an error.

我还有其他一些地方使我对当前代码感到困惑,并以注释表示,因此,我也很感谢那些方面的帮助,解释或想法.

I also have a few other points where I'm confused about my current code, indicated by comments, so I'd appreciate any help, explanation, or ideas in those areas too.

这是我的示例代码:

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 { $0 as Error }
            .map { $0.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()
    }
}

推荐答案

发布失败总是会终止订阅.由于您要在出现错误后继续发布,因此您不能将错误发布为失败.您必须改为更改发布者的输出类型.标准库提供了 Result ,这就是您应该使用的.

Publishing a failure always ends a subscription. Since you want to continue publishing after an error, you cannot publish your error as a failure. You must instead change your publisher's output type. The standard library provides Result, and that's what you should use.

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($0) }
                .catch { Just(Result.failure($0)) }
        })
        .eraseToAnyPublisher()
}

此发布者会定期发出 .success(响应) .failure(错误),并且永远不会失败.

This publisher emits either .success(response) or .failure(error) periodically, and never completes with a failure.

但是,您应该问自己,如果用户反复切换应用程序会怎样?或者,如果API请求需要30秒钟以上才能完成?(或者两者?)您将同时运行多个请求,并且响应将按照它们到达的顺序进行处理,这可能不是发送请求的顺序.

However, you should ask yourself, what happens if the user switches apps repeatedly? Or what if the API request takes more that 30 seconds to complete? (Or both?) You'll get multiple requests running simultaneously, and the responses will be handled in the order they arrive, which might not be the order in which the requests were sent.

解决此问题的一种方法是使用 flatMap(maxPublisher:.max(1)){...} ,这会使 flatMap 忽略计时器和通知信号虽然有一个未完成的请求.但是,对于每个信号启动一个新请求,然后取消先前的请求,可能会更好.将 flatMap 更改为 map ,然后将 switchToLatest 更改为该行为:

One way to fix this would be to use flatMap(maxPublisher: .max(1)) { ... }, which makes flatMap ignore timer and notification signals while it's got a request outstanding. But it would perhaps be even better for it to start a new request on each signal, and cancel the prior request. Change flatMap to map followed by switchToLatest for that behavior:

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($0) }
                .catch { Just(Result<StatusResponse, Error>.failure($0)) }
        })
        .switchToLatest()
        .eraseToAnyPublisher()
}

这篇关于错误后如何继续URLSession dataTaskPublisher或其他Publisher?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆