Created
February 26, 2020 20:52
-
-
Save cenkbilgen/d28a1ab12aca6bdf89c3e3c878052ea8 to your computer and use it in GitHub Desktop.
An extension to URLSession to create a Combine Publisher for URLDownloadTask
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Combine | |
fileprivate class CancellableStore { | |
static let shared = CancellableStore() | |
var cancellables = Set<AnyCancellable>() | |
} | |
public enum DownloadOutput { | |
case complete(Data) | |
case downloading(transferred: Int64 = 0, expected: Int64 = 0) // cumulative bytes transferred, total bytes expected | |
} | |
extension URLSession { | |
public func downloadTaskPublisher(with request: URLRequest) -> AnyPublisher<DownloadOutput, Error> { | |
let subject = PassthroughSubject<DownloadOutput, Error>() | |
let task = downloadTask(with: request) { (tempURL, response, error) in | |
guard error == nil else { | |
subject.send(completion: .failure(error!)) | |
return | |
} | |
guard let httpResponse = response as? HTTPURLResponse else { | |
let error = TransferError.urlError(URLError(.badServerResponse)) | |
subject.send(completion: .failure(error)) | |
return | |
} | |
// handle 304 in an outer layer | |
guard httpResponse.statusCode == 200 else { | |
let error = TransferError.httpError(httpResponse) | |
subject.send(completion: .failure(error)) | |
return | |
} | |
guard let url = tempURL else { | |
let error = TransferError.urlError(URLError(.fileDoesNotExist)) | |
// not the most appropriate error message, but at a low-level that's exactly the error | |
subject.send(completion: .failure(error)) | |
return | |
} | |
do { | |
let data = try Data(contentsOf: url, options: [.dataReadingMapped, .uncached]) | |
subject.send(.complete(data)) | |
subject.send(completion: .finished) | |
} catch { | |
subject.send(completion: .failure(error)) | |
return | |
} | |
} | |
task.taskDescription = request.url?.absoluteString | |
let receivedPublisher = task.publisher(for: \.countOfBytesReceived) | |
.debounce(for: .seconds(0.1), scheduler: RunLoop.current) // adjust | |
let expectedPublisher = task.publisher(for: \.countOfBytesExpectedToReceive, options: [.initial, .new]) | |
Publishers.CombineLatest(receivedPublisher, expectedPublisher) | |
.sink { | |
let (received, expected) = $0 | |
let output = DownloadOutput.downloading(transferred: received, expected: expected) | |
subject.send(output) | |
}.store(in: &CancellableStore.shared.cancellables) | |
task.resume() | |
return subject.eraseToAnyPublisher() | |
} | |
} | |
// MARK: Error Types | |
public enum TransferError: Error { | |
case httpError(HTTPURLResponse) | |
case urlError(URLError) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment