Как я могу продолжить URLSession dataTaskPublisher или другого издателя после ошибки? - PullRequest
0 голосов
/ 05 мая 2020

У меня есть приложение, которое должно проверять статус на сервере:

  • каждые 30 секунд
  • всякий раз, когда приложение выходит на передний план

Я делаю это путем объединения двух издателей, а затем вызываю flatMap вывод объединенного издателя для запуска запроса API.

У меня есть функция, которая выполняет запрос API и возвращает издателю результат, включая logi c, чтобы проверить ответ и выдать ошибку в зависимости от его содержимого.

Кажется, что как только выдается ошибка 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 { $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()
    }
}

Ответы [ 2 ]

1 голос
/ 06 мая 2020

Ошибка публикации всегда завершает подписку. Поскольку вы хотите продолжить публикацию после ошибки, вы не можете publi sh вашу ошибку как сбой. Вместо этого вы должны изменить тип вывода вашего издателя. Стандартная библиотека предоставляет 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($0) }
                .catch { Just(Result.failure($0)) }
        })
        .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($0) }
                .catch { Just(Result<StatusResponse, Error>.failure($0)) }
        })
        .switchToLatest()
        .eraseToAnyPublisher()
}
0 голосов
/ 05 мая 2020

Вы можете использовать retry (), чтобы получить такое поведение или поймать его ... подробнее здесь: https://www.avanderlee.com/swift/combine-error-handling/

...