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.
.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
}
.fileImporter
wraps NSOpenPanel
The same modifier works on macOS.
Use allowsMultipleSelection = true
for batch uploads.
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
.
URLSession
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)
}
}
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
}
}
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.
allowsMultipleSelection
in .fileImporter
.[URL]
and create one task per file, or
zip them into a single multipart payload.
Very large files (>200 MB) should use a
URLSessionConfiguration.background
so the upload continues when the
app goes to the background.
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.
Note — When sharing large collaborative documents you may prefer Apple’s
CloudKit
+ CKShare
instead of rolling your own
endpoint.
// 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
).
File