Pattern · Prompt Input

New

Attachment Composer SwiftUI Block

A message composer that attaches photos and files, uploads each with its own progress ring, and turns Send into Stop.

A composer pinned above the keyboard with a plus menu for Photos, Camera and Files. Each pick lands in a tray as a thumbnail or file chip and uploads through your closure with a ring that fills, or a failure you can retry; Send waits for text or an attachment and every upload, and turns into Stop while the reply runs.

  • SwiftUI
  • iOS 17.0+
  • composer-attachments
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • Photos, Camera and Files from one plus menu
  • A tray of removable thumbnails and file chips
  • Each upload shows a filling ring, done, or failed with Retry
  • A field that grows from one to six lines, with a counter near the limit
  • Send waits for uploads, then turns into Stop while the reply runs

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

The composer sits on one card pinned above the keyboard: a plus button, a field that grows from one line to six, and Send. Plus opens a menu of Photos (the system picker, several at once), Camera when you pass one and the device has it, and Files. Each pick lands in a tray above the field, photos as 64 point thumbnails and documents as chips with their name, type and size, and starts uploading through your closure at once: a thin ring fills from the progress you report, then clears. A failed upload outlines its tile in the danger colour with Retry in its place, and every tile has an x that removes it and cancels its upload. Send stays off until there is text or an attachment and every upload has finished; while your onSend runs, Stop takes its place and cancels the task. A failed send hands the text and attachments back with the error under the card. From 90% of the character limit a counter shows under the field, turning to the danger colour past it, where Send turns off.

Notes

  • AttachmentComposer(text:placeholder:characterLimit:maxAttachments:fileTypes:onUpload:onCamera:onSend:). Only onUpload and onSend are required. It is a section: no navigation stack, background, padding or scroll view of its own. Pin it with .safeAreaInset(edge: .bottom) and .padding(.horizontal, 20), on a theme.color.bg background so the thread scrolls under it cleanly.
  • AttachmentComposer.Attachment is what the tray holds: an id, a kind (.image or .file), the name, contentType (UTType), byteCount, the photo's data or the picked fileURL, and a small JPEG thumbnail. Attachment.image(_:name:) builds one from photo bytes with its thumbnail, for your camera closure. Attachment.sample is a PDF.
  • onUpload receives the attachment and a progress closure: call progress(0.4) as bytes go, return when it's stored, throw to fail. A thrown LocalizedError's description is read by VoiceOver. Removing the tile cancels the task, so check for cancellation. Keep your own map from attachment.id to the stored file.
  • Files hands you a security-scoped fileURL: call startAccessingSecurityScopedResource() before reading it in onUpload and stop after.
  • onCamera presents your camera and returns Attachment.image(data), or nil when the person cancels. Without it, or on a device without a camera, the menu leaves Camera out.
  • onSend receives AttachmentComposer.Message: the text, trimmed, and the uploaded attachments in tray order. The field and tray clear at once. Stop cancels the task and frees the composer immediately, ignoring anything your closure finishes later; throw CancellationError or let Task.sleep and URLSession do it. Any other error hands the message back and shows the error's errorDescription, or "Couldn't send. Try again.", under the card.
  • text is a Binding<String> for a draft your app keeps per conversation. characterLimit is 4,000 by default; nil removes the counter. maxAttachments is 6; the plus button dims at the limit. fileTypes is what Files offers.
  • Photos runs out of process and needs no permission. Picked photos show a spinner tile while their bytes load.
  • Haptics: a selection tick when an attachment lands, a light impact on Send, an error when an upload or send fails.
  • VoiceOver reads each tile by name with its state ("Uploading, 40 percent", "Uploaded" or the failure) and offers Retry upload and Remove as actions. Send reads why it is off while uploads run. Stop and failures are announced. Every control has a 44 point target.
  • Reduce Motion turns the tray's arrivals and the ring's fill into short fades.
  • AttachmentComposerExample at the end of AttachmentComposer.swift is the Planning thread from the preview: two turns, then the composer pinned above the keyboard. Uploads fill over about a second; a file with "fail" in its name stops partway so Retry can be tried; sending holds the reply for a few seconds so Stop can be tried. Pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The card is color.surface with radius.lg and a 1 point separator hairline that turns to textPrimary while the field is focused. The field is type.body in textPrimary with its prompt in textTertiary.
  • Plus is a 36 point fill circle with a textPrimary symbol in a 44 point target. Send is the only accent: an accent circle with an accentOn arrow, fill with a textTertiary arrow while off, and the same circle with a stop square while the reply runs.
  • Thumbnails use radius.sm; the upload ring is white over a dark scrim on photos and textPrimary on a separator track on file chips, which sit on fill. Names are type.subhead, details type.caption in textSecondary. Failures use danger.
  • The counter is type.caption with monospaced digits; errors are type.footnote in danger.
  • State changes use motion.smooth and motion.snappy, and become short fades under Reduce Motion.

Requirements

  • NSCameraUsageDescription only if your onCamera closure presents the camera. Photos and Files need no Info.plist keys.