Chapter 9 — Uploading & downloading files

Uploading with multipart/form-data

// Chirp/Networking/MultipartFormData.swift
import Foundation

struct MultipartFormData {
    let boundary = "Boundary-\(UUID().uuidString)"
    private var body = Data()

    var contentType: String { "multipart/form-data; boundary=\(boundary)" }

    mutating func addFile(name: String, filename: String, mimeType: String, data: Data) {
        body.appendString("--\(boundary)\r\n")
        body.appendString("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(filename)\"\r\n")
        body.appendString("Content-Type: \(mimeType)\r\n\r\n")
        body.append(data)
        body.appendString("\r\n")
    }

    func finalized() -> Data {
        var result = body
        result.appendString("--\(boundary)--\r\n")   // closing boundary
        return result
    }
}

private extension Data {
    mutating func appendString(_ string: String) { append(Data(string.utf8)) }
}
// Chirp/Features/Profile/AvatarUploader.swift
func uploadAvatar(_ imageData: Data, client token: String) async throws -> ProfileDTO {
    var form = MultipartFormData()
    form.addFile(name: "file", filename: "avatar.jpg", mimeType: "image/jpeg", data: imageData)

    var request = URLRequest(url: baseURL.appendingPathComponent("me/avatar"))
    request.httpMethod = "POST"
    request.setValue(form.contentType, forHTTPHeaderField: "Content-Type")   // includes the boundary
    request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

    let (data, response) = try await session.upload(for: request, from: form.finalized())
    try validate(response, data: data)
    return try decoder.decode(ProfileDTO.self, from: data)
}
  • Compress the image first. A photo from the camera can be 10 MB; that's slow to upload and the
  • Use upload(for:from:), not a JSON body. The upload variant streams the body efficiently and is

Reporting upload progress

final class UploadProgressDelegate: NSObject, URLSessionTaskDelegate {
    let onProgress: @Sendable (Double) -> Void
    init(onProgress: @escaping @Sendable (Double) -> Void) { self.onProgress = onProgress }

    func urlSession(_ session: URLSession, task: URLSessionTask,
                    didSendBodyData bytesSent: Int64,
                    totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        guard totalBytesExpectedToSend > 0 else { return }
        let fraction = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
        onProgress(fraction)      // 0.0 → 1.0
    }
}

// Pass it to the specific upload:
let progress = UploadProgressDelegate { fraction in
    Task { @MainActor in viewModel.uploadProgress = fraction }   // drive a ProgressView
}
let (data, response) = try await session.upload(for: request, from: body, delegate: progress)

Downloading and caching images

  • URLCache (Chapter 7) — the raw bytes, on disk/memory, respecting HTTP headers. Shared by all
  • A decoded-image memory cache (NSCache<NSURL, UIImage>) — the decoded image, so you skip the
  • In-flight deduplication — if two rows request the same URL at once, do one download, not two.
// Chirp/Networking/ImageLoader.swift
import UIKit

actor ImageLoader {
    static let shared = ImageLoader()
    private let cache = NSCache<NSURL, UIImage>()
    private var inFlight: [URL: Task<UIImage, Error>] = [:]

    func image(for url: URL) async throws -> UIImage {
        if let cached = cache.object(forKey: url as NSURL) { return cached }   // decoded cache hit
        if let existing = inFlight[url] { return try await existing.value }     // dedup concurrent loads

        let task = Task { () throws -> UIImage in
            defer { inFlight[url] = nil }
            let (data, _) = try await URLSession.shared.data(from: url)         // URLCache underneath
            guard let image = UIImage(data: data) else { throw APIError.decoding(URLError(.cannotDecodeContentData)) }
            cache.setObject(image, forKey: url as NSURL)
            return image
        }
        inFlight[url] = task
        return try await task.value
    }
}
struct CachedImage: View {
    let url: URL?
    @State private var image: UIImage?

    var body: some View {
        Group {
            if let image { Image(uiImage: image).resizable() }
            else { Color.gray.opacity(0.2) }            // placeholder
        }
        .task(id: url) {
            guard let url else { return }
            image = try? await ImageLoader.shared.image(for: url)
        }
    }
}

Downloading larger files

let (tempURL, response) = try await session.download(for: request)
try validate(response, data: Data())
// Move it out of the temp location before the closure returns — the OS deletes temp files.
let destination = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("export.zip")
try? FileManager.default.removeItem(at: destination)
try FileManager.default.moveItem(at: tempURL, to: destination)

Background transfers (for when the app is suspended)

let config = URLSessionConfiguration.background(withIdentifier: "com.chirp.uploads")
config.isDiscretionary = false          // start promptly (true lets the OS wait for good conditions)
let backgroundSession = URLSession(configuration: config, delegate: myDelegate, delegateQueue: nil)
flowchart TB subgraph Up_Upload___Upload__ ["Up_Upload ["Upload"]"] Pick["pick + compress image"] --> Multi["build multipart body"] --> UpTask["session.upload (progress via delegate)"] --> Prof["→ ProfileDTO"] end subgraph Down_Download___Download__ ["Down_Download ["Download"]"] Row["feed row needs image"] --> Loader["ImageLoader: NSCache → URLCache → network"] --> Img["decoded UIImage"] end

What we built

  • Built multipart/form-data upload for the avatar, with a reusable body builder, image
  • Reported upload progress with a URLSessionTaskDelegate (didSendBodyData) driving a
  • Built a caching ImageLoader (decoded NSCache + in-flight dedup over URLCache) and a
  • Used download(for:) for large files (streamed to disk) and covered background sessions for

Mental model to take away

  • Uploads use multipart/form-data (build the body carefully) and upload(for:from:); compress
  • For images, cache at three levels: URLCache (bytes), an NSCache of decoded images, and
  • Use data(...) for small in-memory responses, download(...) for big files streamed to disk.
  • Reach for a background URLSession only for transfers that must outlive the foreground — it trades