6

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

2 Answers 2

3

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() } } 
Sign up to request clarification or add additional context in comments.

3 Comments

It seems that retry is used in cases like "I failed to load smth while online". In case of unreachable connection you publisher will try to publish response for retry(2) - 3 times and then finishes with error, isn't it? Btw it's interesting solution with progress, I'll give it a try some day
Please test the solution and mark the answer as correct if it works, that way other users can know.
Using .publisher(for: \.progress.completedUnitCount) seems to raise "BUG IN CLIENT OF LIBPLATFORM: Trying to recursively lock an os_unfair_lock" what does work for me is: task.publisher(for: \.progress).flatMap { $0.publisher(for: \.fractionCompleted )}
-1

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.