CodeKits.shopNew developer offer

Add photo and file attachments to SwiftUI

Separate photo and document selection from upload, stage private files safely, and show loading, cancellation and unsupported-format failures.

By Jonathan · 4 min read

Published

Photo and file pickers feed a private staging area before upload
In this article

Selecting an attachment is not the same as having its bytes ready. A photo may still be in iCloud, and a document URL may require temporary security-scoped access. Treat selection, staging and upload as separate states so the interface can explain failures without losing the draft message.

This tutorial targets iOS 16 or later with SwiftUI, PhotosUI and UniformTypeIdentifiers. It gives bounded integration excerpts for a small attachment composer, not a complete messaging app. The local toolchain can type-check Swift APIs; provider permissions, iCloud downloads and device memory behaviour require a signed app and real-device tests.

Use the picker that matches the source

Use PhotosPicker for user-selected photos and fileImporter for documents. A system photo picker avoids requesting broad library access just to select an image. Apple's Photos picker sample also explains that the selected item is initially a placeholder and loading can fail.

AttachmentPicker.swift
import SwiftUI
import PhotosUI
import UniformTypeIdentifiers
 
struct AttachmentPicker: View {
    @State private var photo: PhotosPickerItem?
    @State private var showFiles = false
    let selectedPhoto: (PhotosPickerItem) -> Void
    let selectedFile: (Result<URL, Error>) -> Void
 
    var body: some View {
        HStack {
            PhotosPicker("Photo", selection: $photo, matching: .images)
            Button("File") { showFiles = true }
        }
        .onChange(of: photo) { item in
            if let item { selectedPhoto(item) }
        }
        .fileImporter(isPresented: $showFiles,
                      allowedContentTypes: [.pdf, .plainText]) { result in
            selectedFile(result)
        }
    }
}

The callbacks deliberately hand work to a model rather than loading files in the view body. Keep a selection generation ID or cancellable task in that model. If the user picks another item while the first is loading, the earlier result must not overwrite the newer selection.

Stage documents while access is valid

Copy documents into a private, app-owned staging directory before releasing security-scoped access. Create that directory explicitly and use generated filenames, not user filenames as filesystem paths. Keep the display filename separately.

StageDocument.swift
import Foundation
 
func stageDocument(from source: URL, into directory: URL) throws -> URL {
    guard source.startAccessingSecurityScopedResource() else {
        throw CocoaError(.fileReadNoPermission)
    }
    defer { source.stopAccessingSecurityScopedResource() }
    let values = try source.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey])
    guard values.isRegularFile == true, let size = values.fileSize,
          size <= 5 * 1024 * 1024 else { throw CocoaError(.fileReadTooLarge) }
    let destination = directory.appendingPathComponent(UUID().uuidString)
    try FileManager.default.copyItem(at: source, to: destination)
    return destination
}

This minimal helper is for a trusted picker URL and a small-file policy. File metadata can change, so a production staging adapter should coordinate provider reads, enforce the byte limit during copying, check the final stored size, and clean up partial copies on failure. Move blocking file operations off the main actor. Apple's file importer contract describes security-scoped URLs.

Load photos without accidental memory spikes

For small images, loadTransferable(type: Data.self) is convenient, but checking its size after loading is not a hard peak-memory limit. For large assets use a custom Transferable with FileRepresentation and copy the provided temporary file into staging while it is available. Keep only a downsampled thumbnail in view state.

Decide whether to preserve original formats or transcode. A chosen image may be HEIC, not JPEG. If the server accepts only JPEG, convert deliberately and label the resulting media type correctly. Consider metadata removal for location privacy. Client filtering improves UX; the server must still validate type, size, ownership and content.

Test selection independently from upload

Cancel each picker: the existing draft should remain unchanged. Select an iCloud-only asset while offline: show a retryable load failure. Select an unsupported document or oversized file: explain the policy before uploading. Pick A, then B quickly: B must remain selected even if A finishes last.

Test deleting a staged attachment while no upload is running, and deleting one during an upload. Coordinate ownership so cleanup cannot remove a file still being read. On app restart, reconcile abandoned staging files against persisted draft and transfer records rather than deleting the entire staging directory.

Troubleshooting and next steps

Permission errors usually mean the provider URL was used after scoped access ended. Empty previews can mean the photo has not loaded yet. Crashes with large photos often indicate full-resolution decoding on the main actor.

Continue with large iOS uploads. For a complete native attachment workflow, see CodeKits Swift iOS development.