-
Notifications
You must be signed in to change notification settings - Fork 1
Uploads Downloads and Progress
Husnain Ali edited this page Sep 27, 2026
·
3 revisions
var form = MultipartFormData()
form.append("A caption", name: "caption")
form.append(jpegData, name: "photo", fileName: "cat.jpg", mimeType: "image/jpeg")
form.append(fileURL, name: "video") // streamed from disk, not loaded into memory
let created: Photo = try await client.upload(CreatePhoto(), from: .multipart(form)) { event in
print(event.fraction ?? 0) // 0.0 ... 1.0
}- RFC 7578 boundary construction, per-part
Content-Type,Content-Disposition. - Large file parts stream from disk.
- Uploads are not retried and skip the 401-refresh hop; a partial upload is unsafe to replay.
Response interceptors still run (a
.retryoutcome is ignored, since a transfer is never re-sent). - An in-memory body can also be sent with
.dataor.file:
try await client.upload(PutAvatar(), from: .data(pngData))
try await client.upload(PutAvatar(), from: .file(localURL))let destination = URL.documentsDirectory.appending(path: "report.pdf")
let fileURL = try await client.download(GetExport(), to: destination) { event in
print("\(event.completed) / \(event.total) bytes")
}- Pass
to:for a specific destination, or omit it to get the transport's temp file URL (move it before the next run). - Cancellation via
Taskcancellation or request ID, same as any request. - Background
URLSessiontransfers need app-side wiring and are not handled automatically. See Platform Notes Thread Safety and Performance. - If a response interceptor fails the transfer, or the finished file cannot be moved out of
URLSession's temporary location, the caller never gets a URL to a file that doesn't exist.
Both upload and download take a trailing @Sendable (ProgressEvent) -> Void closure.
public struct ProgressEvent: Sendable, Hashable {
public let completed: Int64
public let total: Int64 // -1 if the server sent no Content-Length
public var fraction: Double? // completed / total, or nil when total is unknown
}The closure is called on an arbitrary executor; hop to the main actor for UI:
try await client.download(GetExport(), to: dest) { event in
Task { @MainActor in self.progress = event.fraction ?? 0 }
}For Combine, uploadPublisher / downloadPublisher emit .progress(ProgressEvent) values then a
final .finished(...). See Combine and SwiftUI.
Best practice: hop progress and completion callbacks to @MainActor before touching UI state.
SwiftNetworkKit is source-available proprietary software (not open source). See FAQ Licensing and Support. Repo: https://github.com/ihusnainalii/SwiftNetworkKit · Docs: https://swiftnetworkkit.vercel.app/
Getting started
Guides
- Defining Endpoints
- Authentication and Token Refresh
- Security and Certificate Pinning
- Retry Policy
- Interceptors
- Request Management
- Caching
- Offline Request Queue
- Uploads Downloads and Progress
- Pagination and Batch
- Combine and SwiftUI
- Error Handling
- Observability
- Testing and Mocking
- Best Practices and a Complete Example
- Platform Notes, Thread Safety and Performance
Reference