How to upload large files properly from iOS / VisionOS to an endpoint

Sravan KaruturiSravan Karuturi
5 min read

Introduction

Last year, I wanted to make an application that would upload large video files to our database from the client application. One issue I faced when implementing it is that if the upload takes longer than a few minutes and if the user closes the app, the upload pauses. For the Vision Pro, we expect the users to click upload and remove the headset. So, how do we make sure that it goes through?

After this article, you’ll have a better understanding of how to create, run and make use of background tasks in an app. You’ll have a functioning upload manager to integrate with your applications.

How to do it right?

If you have a large file that you want to upload from your device to a server, there are a few precautions you need to take to make sure it happens smoothly.

To do this, we have to make sure that the device does not go to sleep and stop the upload.

In iOS, we can make this happen by

  1. letting the system know that we will be running background tasks.

  2. Creating the background tasks and running them.

Letting the system know that we will be running background tasks

Doing this is fairly straightforward. Open up your xcode project and select your target.

Go to the signing and capabilities tab and click on the + Capability button.

Then search for background modes. Please note that if your app already has that capability in, it will not show here.

Once this is done, make sure that the background fetch and background processing capabilities are selected here.

Now we’re ready for the next step.

Creating background tasks and running them

To understand creating background upload tasks, we have to look at the URLSession types available to us.

  1. Default Session (URLSessionConfiguration.default)

This is suitable for most networking tasks.

Behavior:

  • Stores data to a persistent cache (if configured).

  • Persists cookies.

  • Uses shared credentials storage by default.

2. Ephemeral Session (URLSessionConfiguration.ephemeral)

Ideal when you want in-memory-only storage of cookies, caches, or credentials.

Behavior:

  • Does not write any data (e.g., cookies, cache, credentials) to disk.

  • Everything is stored in RAM and is gone once your app terminates or the session is invalidated.

3. Background Session (URLSessionConfiguration.background(_:))

Used for performing network operations in the background, even if the app is suspended or terminated. Great for large file uploads/downloads.

Behavior:

  • Performs transfers in a separate process.

  • Allows transfers to continue running when the app is in the background.

  • Discretionary nature: the system can schedule transfers for optimal performance and power efficiency.

4. Shared Session (URLSession.shared)

A convenient, pre-configured singleton session with the default configuration.

Behavior:

  • No ability to customize.

  • Good for quick, stateless network requests where no additional configuration is necessary.

As you might realize, we want to use the Background Session for our situation. I tend to use Shared Session for most of my tasks and I quickly realized that when the user removes the headset ( Vision Pro ), the app is put to sleep and the upload tasks I started did not finish.

To fix that, we need to Background Session

To create a background session, we want to create a BackgroundDelegate class that conforms to URLSessionTaskDelegate and URLSessionDelegate

Once we have that BackgroundDelegate class defined, we will create a configuration for our URLSession by using URLSessionConfiguration.background(withIdentifier: "")

Here, the identifier has to be unique and can be used to identify which tasks are running in the background etc.

We have to add the background id we used here, in the Info.plist file.

Please also make sure that the Required background modes are set here as shown in the picture above.

Once we have the session configuration variable, we now create the actual session that we’re going to use. We pass in the configuration and the delegate, and the delegateQueue

let bgSession = URLSession(configuration: configuration, delegate: BackgroundDelegate(completion: completion), delegateQueue: .main)

/// This class will be the delegate handler for the task/url session
        final class BackgroundDelegate: NSObject, URLSessionTaskDelegate, URLSessionDelegate {

            let completion: @Sendable (Result<Void, RestAPIError>, HTTPURLResponse?) -> Void

            init(completion: @escaping @Sendable (Result<Void, RestAPIError>, HTTPURLResponse?) -> Void) {
                self.completion = completion
            }

            func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {

                let response = task.response

                if let error = error {
                    completion(.failure(.unknown), nil)
                    return
                }

                guard let response else {
                    completion(.failure(.noResponse), nil)
                    return
                }

                guard let httpResponse = response as? HTTPURLResponse else {
                    completion(.failure(.unknownResponse), nil)
                    return
                }

                if httpResponse.isResponseOk {
                    completion(.success(()), httpResponse)
                } else {
                    completion(.failure(.nonOkResponse(statusCode: httpResponse.statusCode)), httpResponse)
                }

            }

        }

        let configuration = URLSessionConfiguration.background(withIdentifier: "com.TrebleCore.Upload") // This name has to be unique.
        configuration.timeoutIntervalForRequest = 3600 // We set the time limit for the upload to 1 hr.
        configuration.httpMaximumConnectionsPerHost = 1

        let bgSession = URLSession(configuration: configuration, delegate: BackgroundDelegate(completion: completion), delegateQueue: .main)
        let task = bgSession.uploadTask(with: request, fromFile: documentsFileUrl)
        task.resume()

The total code for the upload is shown above.

One important thing to keep in mind is that we cannot use ephemeral locations like the temp directory to upload files from when it is a background task. So, I wrote a small function to copy it to the documents folder and upload it from there.

Copy to the documents folder:

    func copyFileToDocumentsDirectory(from fileURL: URL) -> URL? {

        let fileManager = FileManager.default
        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
        let destinationURL = documentsDirectory.appendingPathComponent(fileURL.lastPathComponent)

        do {
            if fileManager.fileExists(atPath: destinationURL.path) {
                try fileManager.removeItem(at: destinationURL)
            }
            try fileManager.copyItem(at: fileURL, to: destinationURL)
            return destinationURL
        } catch {
            SLogger.error("Failed to copy the file: error= \(error.localizedDescription)")
            return nil
        }

    }

Combined with both copying it over to the documents directory and creating a background task and running it, we’ll make sure the upload is handled by the OS — even if your app is in the background and the device wants to go to sleep etc.

Please let me know if you face any issues with uploading files in the background!

0
Subscribe to my newsletter

Read articles from Sravan Karuturi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sravan Karuturi
Sravan Karuturi