Implementing reconnection with URLSession publisher and Combine

只谈情不闲聊 提交于 2020-07-19 04:04:10

问题


I'm wondering if there is a way to implement reconnection mechanism with new Apple framework Combine and use of URLSession publisher

  • tried to find some examples in WWDC 2019
  • tried to play with waitsForConnectivity with no luck (it even not calling delegate on custom session)
  • tried URLSession.background but it crashed during publishing.

I'm also not understanding how do we track progress in this way
Does anyone already tried to do smth like this?

upd:
It seems like waitsForConnectivity is not working in Xcode 11 Beta

upd2:
Xcode 11 GM - waitsForConnectivity is working but ONLY on device. Use default session, set the flag and implement session delegate. Method task is waiting for connectivity will be invoked no matter if u r using init task with callback or without.

public class DriverService: NSObject, ObservableObject {

    public var decoder = JSONDecoder()
    public private(set) var isOnline = CurrentValueSubject<Bool, Never>(true)

    private var subs = Set<AnyCancellable>()
    private var base: URLComponents
    private  lazy var session: URLSession = {
        let config = URLSessionConfiguration.default
        config.waitsForConnectivity = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    public init(host: String, port: Int) {

        base = URLComponents()
        base.scheme = "http"
        base.host = host
        base.port = port

        super.init()

//      Simulate online/offline state
//
//        let pub = Timer.publish(every: 3.0, on: .current, in: .default)
//        pub.sink { _ in
//            let rnd = Int.random(in: 0...1)
//            self.isOnline.send(rnd == 1)
//        }.store(in: &subs)
//        pub.connect()
    }

    public func publisher<T>(for driverRequest: Request<T>) -> AnyPublisher<T, Error> {

        var components = base
        components.path = driverRequest.path

        var request = URLRequest(url: components.url!)
        request.httpMethod = driverRequest.method

        return Future<(data: Data, response: URLResponse), Error> { (complete) in
            let task = self.session.dataTask(with: request) { (data, response, error) in
                if let err = error {
                    complete(.failure(err))
                } else {
                    complete(.success((data!, response!)))
                }
                self.isOnline.send(true)
            }
            task.resume()
        }
        .map({ $0.data })
        .decode(type: T.self, decoder: decoder)
        .eraseToAnyPublisher()
    }
}

extension DriverService: URLSessionTaskDelegate {

    public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
        self.isOnline.send(false)
    }

}

回答1:


Have you tried retry(_:) yet? It’s available on Publishers and reruns the request upon failure.

If you don’t want the request to immediately rerun for all failures then you can use catch(_:) and decide which failures warrant a rerun.

Here's some code to achieve getting the progress.

enum Either<Left, Right> {
    case left(Left)
    case right(Right)

    var left: Left? {
        switch self {
        case let .left(value):
            return value
        case .right:
            return nil
        }
    }

    var right: Right? {
        switch self {
        case let .right(value):
            return value
        case .left:
            return nil
        }
    }
}

extension URLSession {
    func dataTaskPublisherWithProgress(for url: URL) -> AnyPublisher<Either<Progress, (data: Data, response: URLResponse)>, URLError> {
        typealias TaskEither = Either<Progress, (data: Data, response: URLResponse)>
        let completion = PassthroughSubject<(data: Data, response: URLResponse), URLError>()
        let task = dataTask(with: url) { data, response, error in
            if let data = data, let response = response {
                completion.send((data, response))
                completion.send(completion: .finished)
            } else if let error = error as? URLError {
                completion.send(completion: .failure(error))
            } else {
                fatalError("This should be unreachable, something is clearly wrong.")
            }

        }
        task.resume()
        return task.publisher(for: \.progress.completedUnitCount)
            .compactMap { [weak task] _ in task?.progress }
            .setFailureType(to: URLError.self)
            .map(TaskEither.left)
            .merge(with: completion.map(TaskEither.right))
            .eraseToAnyPublisher()
    }
}



回答2:


I read your question title several times. If you mean reconnect the URLSession's publisher. Due to the URLSession.DataTaskPublisher has two results. Success output or Failure (a.k.a URLError). It's not possible to make it reconnect after the output produced.

You can declare one subject. e.g

let output = CurrentValueSubject<Result<T?, Error>, Never>(.success(nil))

And add a trigger when network connection active then request resources and send the new Result to the output. Subscribe output in the other place. So that you can get new value when network back-online.



来源:https://stackoverflow.com/questions/57836518/implementing-reconnection-with-urlsession-publisher-and-combine

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