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

孤者浪人 提交于 2021-01-07 02:43:44

问题


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

  • every 30 seconds
  • whenever the app enters the foreground

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

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.

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.

Here's my example code:

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()
    }
}

回答1:


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()
}

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

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.

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()
}



回答2:


You can use retry() to get this kind of behaviour or catch it...more info here: https://www.avanderlee.com/swift/combine-error-handling/



来源:https://stackoverflow.com/questions/61604322/how-can-i-continue-urlsession-datataskpublisher-or-another-publisher-after-error

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!