# Upload Files in iOS Using Combine: Progress Tracking Guide

[Gleb Arkhipov](https://www.strv.com/blog/authors/gleb)iOS Engineering Manager

---

Have you ever wondered how to upload files using Combine in iOS? Or maybe how to track their progress? After doing some research, I wasn’t able to find anything relevant to this topic, so I had to put in the time myself. I’d now like to share my findings with you.

Let’s start from scratch and build our solution block by block. Apple did a good job of adding Combine publishers everywhere across their system frameworks. Unfortunately, `URLSession` only provides:

```swift
func dataTaskPublisher(for url: URL) -> URLSession.DataTaskPublisher
```

which can be used for regular requests but is not sufficient for file uploads. Instead, we will use a standard API and wrap it into a publisher to make it compatible with Combine pipelines.

```swift
func uploadTask(
    with request: URLRequest,
    fromFile fileURL: URL,
    completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionUploadTask
```

Please note that you can also go for another method that uses `Data` as opposed to `URL`; however, this approach is not recommended since the whole file will be kept in memory, increasing the application footprint. This can have a negative impact on performance, especially in case of larger files like videos. We will create a dreaded `APIManager` to demonstrate the solution — but feel free to experiment!

```swift
class APIManager: NSObject {
    func upload(request: URLRequest, fileURL: URL) -> ?
}
```

What is the return type, you might ask? Well, there are three things we need to track: Successful upload, its progress and errors.

One option will be to go for something like `(AnyPublisher<Data?, Error>, AnyPublisher<Double, Never>)`. Despite allowing some flexibility with the individual publishers, I prefer something more elegant — `AnyPublisher<UploadResponse, Error>` — where the response slightly resembles a Swift result type.

```swift
enum UploadResponse {
    case progress(percentage: Double)
    case response(data: Data?)
}
```

Now we are able to tackle the uploading part.

```swift
class APIManager: NSObject {

    func upload(
        request: URLRequest,
        fileURL: URL
    ) -> AnyPublisher<UploadResponse, Error> {

        let subject: PassthroughSubject<UploadResponse, Error> = .init()

        let task: URLSessionUploadTask = URLSession.shared.uploadTask(
            with: request,
            fromFile: fileURL
        ) { data, response, error in
            if let error = error {
                subject.send(completion: .failure(error))
                return
            }
            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
                subject.send(.response(data: data))
                subject.send(completion: .finished)
                return
            }
        }
        task.resume()
        return subject.eraseToAnyPublisher()
    }
}
```

Basically, we will create a `PassthroughSubject` or a `Future` that will emit events once a completion closure is triggered. Of course, response and error handling may vary based on the actual API. Finally, we should take care of progress updates during our upload. Apple provides `URLSessionTaskDelegate` to observe those changes.

```swift
func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    didSendBodyData bytesSent: Int64,
    totalBytesSent: Int64, totalBytesExpectedToSend: Int64
)
```

Let's create a custom `URLSession` and a publisher in order to track the progress.

```swift
let progress: PassthroughSubject<(id: Int, progress: Double), Never> = .init()
lazy var session: URLSession = {
    .init(configuration: .default, delegate: self, delegateQueue: nil)
}()
```

We will also wire it up to the delegate, like this.

```swift
func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    didSendBodyData bytesSent: Int64,
    totalBytesSent: Int64, totalBytesExpectedToSend: Int64
) {
    progress.send((
        id: task.taskIdentifier,
        progress: task.progress.fractionCompleted
    ))
}
```

The only thing still missing is to connect both flows together. Gladly, Combine allows us to do it almost effortlessly.

```swift
return progress
// Otherwise, we will receive events from all the current tasks
    .filter { $0.id == task.taskIdentifier }
    .setFailureType(to: Error.self) // Just to make the compiler happy :)
    .map { .progress(percentage: $0.progress) }
    .merge(with: subject)
    .eraseToAnyPublisher()
```

Here is the final solution handling file uploads and tracking their progress at the same time.

```swift
class APIManager: NSObject {
    let progress: PassthroughSubject<(id: Int, progress: Double), Never> = .init()

    lazy var session: URLSession = {
        .init(configuration: .default, delegate: self, delegateQueue: nil)
    }()

    func upload(request: URLRequest, fileURL: URL) -> AnyPublisher<UploadResponse, Error> {
        // implementation as above
    }
}

extension APIManager: URLSessionTaskDelegate {
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64, totalBytesExpectedToSend: Int64
    ) {
        progress.send((
            id: task.taskIdentifier,
            progress: task.progress.fractionCompleted
        ))
    }
}
```

Now you are able to integrate this uploading publisher into any of the Combine flows. Thank you for sticking with me until the end!

```swift
apiManager.upload(request: request, fileURL: url)
    .receive(on: OperationQueue.main)
    .sink(receiveCompletion: { completion in
        if case let .failure(error) = completion {
            // Handle error
        }
    }) { uploadResponse in
        switch uploadResponse {
        case let .progress(percentage):
            // Handle progress
        case let .response(data):
            // Handle response
        }
    }
```

P.S. Don't forget to use `.receive(on: OperationQueue.main)` after subscribing to this publisher, since changing progress will most likely result in UI updates. :)