# 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 (response as? HTTPURLResponse)?.statusCode == 200 {
	subject.send(.response(data: data))
	subject.send(completion: .finished)
}

}

task.resume()

return subject.eraseToAnyPublisher()
}
```

Basically, we will create a `PassthroughSubject` 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: Double(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> {
	let subject: PassthroughSubject<UploadResponse, Error> = .init()
	let task: URLSessionUploadTask = session.uploadTask(
		with: request,
		fromFile: fileURL
	) { data, response, error in
		if let error = error {
			subject.send(completion: .failure(error))
			return
		}
		if (response as? HTTPURLResponse)?.statusCode == 200 {
			subject.send(.response(data: data))
			subject.send(completion: .finished)
		}
	}
	task.resume()

	return progress
		.filter { $0.id == task.taskIdentifier }
		.setFailureType(to: Error.self)
		.map { .progress(percentage: $0.progress) }
		.merge(with: subject)
		.eraseToAnyPublisher()
}
}

extension APIManager: URLSessionTaskDelegate {
	func urlSession(
		_ session: URLSession,
		task: URLSessionTask,
		didSendBodyData bytesSent: Int64,
		totalBytesSent: Int64,
		totalBytesExpectedToSend: Int64
	) {
		progress.send((
			id: task.taskIdentifier,
			progress: Double(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. :)