Lex
Lex

Reputation: 310

Implementing reconnection with URLSession publisher and Combine

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

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

}

Upvotes: 6

Views: 1880

Answers (2)

user25917
user25917

Reputation: 977

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.

Upvotes: -1

Charles Maria
Charles Maria

Reputation: 2195

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

Upvotes: 3

Related Questions