Uploading Files in SwiftUI

1 · Overview

Scope

File uploads in SwiftUI involve two main tasks: picking a file on‑device and sending its data to a remote server (or local Core Data store). This guide focuses on iOS 17/macOS 15 using async / await and URLSession.

Note — SwiftUI itself has no direct file‑upload API; we compose SwiftUI views with UIKit‑based UIDocumentPickerViewController (iOS) or AppKit‑based NSOpenPanel (macOS), accessed via the .fileImporter modifier.

2 · Selecting a File

2.1 iOS — .fileImporter


// MARK: - FileImporter sample (iOS & iPadOS)
struct FilePickerView: View {
    @State private var selectedURL: URL?

    var body: some View {
        VStack {
            Button("Choose file") { showingImporter.toggle() }
                .fileImporter(
                    isPresented: $showingImporter,
                    allowedContentTypes: [.item],          // UTI/type filter
                    allowsMultipleSelection: false
                ) { result in
                    do { selectedURL = try result.get().first }
                    catch { print(error.localizedDescription) }
                }
        }
    }

    @State private var showingImporter = false
}
      

2.2 macOS — .fileImporter wraps NSOpenPanel

The same modifier works on macOS. Use allowsMultipleSelection = true for batch uploads.

3 · Preparing the Upload Request

3.1 Multipart form‑data

Many back‑ends (e.g. Vapor, Express + Multer) expect a multipart/form-data body. We manually assemble the boundary and payload:


// Build a Data body from file URL
func multipartBody(fileURL: URL,
                   fieldName: String,
                   boundary: String) throws -> Data {

    let filename   = fileURL.lastPathComponent
    let mimeType   = "application/octet-stream"   // or derive via UTType
    
    var body = Data()
    body.append("--\(boundary)\r\n")
    body.append(
        "Content-Disposition: form-data; name=\"\(fieldName)\"; " +
        "filename=\"\(filename)\"\r\n")
    body.append("Content-Type: \(mimeType)\r\n\r\n")
    body.append(try Data(contentsOf: fileURL))
    body.append("\r\n--\(boundary)--\r\n")
    return body
}
      

Note — Data.append(_:) above is a tiny helper extension that converts String to Data using .utf8.

4 · Uploading with URLSession

4.1 Modern async / await API


// MARK: - Perform the upload
func upload(fileURL: URL) async throws {
    let boundary  = UUID().uuidString
    let body      = try multipartBody(
        fileURL: fileURL,
        fieldName: "file",
        boundary: boundary)

    var request   = URLRequest(url: URL(string: "https://example.com/upload")!)
    request.httpMethod        = "POST"
    request.httpBody          = body
    request.setValue("multipart/form-data; boundary=\(boundary)",
                     forHTTPHeaderField: "Content-Type")

    let (_, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
}
      

4.2 Integrating with SwiftUI

Wrap the upload(_:) call in a Task launched from onSubmit or a button’s action:


Button("Upload") {
    guard let url = selectedURL else { return }
    Task {
        do { try await upload(fileURL: url) }
        catch { print(error) }               // present alert in production
    }
}
      

5 · Showing Progress

5.1 Using URLSessionUploadTask

For granular progress, switch to URLSession.uploadTask(with:fromFile:) and observe a Progress instance via .onReceive.


@State private var progress: Double = 0

func uploadWithProgress(fileURL: URL) {
    let boundary = UUID().uuidString
    let request  = /* ...same as above... */
    let session  = URLSession(configuration: .default,
                              delegate: ProgressDelegate($progress),
                              delegateQueue: nil)

    let task     = session.uploadTask(with: request, fromFile: fileURL)
    task.resume()
}

/// Delegate forwarding progress to binding
final class ProgressDelegate: NSObject, URLSessionTaskDelegate {
    @Binding var value: Double
    init(_ binding: Binding) { _value = binding }

    func urlSession(_ session: URLSession, task: URLSessionTask,
                    didSendBodyData bytesSent: Int64,
                    totalBytesSent: Int64,
                    totalBytesExpectedToSend: Int64) {
        value = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
    }
}
      

Bind the progress state to a ProgressView for visual feedback.

6 · Multiple & Large Files

6.1 Batch Uploads

6.2 Background Sessions

Very large files (>200 MB) should use a URLSessionConfiguration.background so the upload continues when the app goes to the background.

7 · Advanced APIs (iOS 17+)

7.1 Transferable

The new Transferable protocol automates file representation (NSItemProvider) and allows tight integration with ShareLink and AirDrop. For uploads you still convert the value to Data before building the request.

7.2 SharePlay / Collaboration

Note — When sharing large collaborative documents you may prefer Apple’s CloudKit + CKShare instead of rolling your own endpoint.

8 · Server Side (Vapor)

8.1 Route Handler


// routes.swift (Vapor 4)
app.on(.POST, "upload", body: .collect(maxSize: "40mb")) { req -> HTTPStatus in
    struct Payload: Content { var file: File }
    let data = try req.content.decode(Payload.self)
    try data.file.write(to: "/uploads/\(data.file.filename)")
    return .ok
}
      

Ensure NGINX/Apache, if present, allows large payloads (client_max_body_size / LimitRequestBody).

9 · Further Reading