Pattern · Prompt Input
NewAttachment 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
$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:). OnlyonUploadandonSendare 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 atheme.color.bgbackground so the thread scrolls under it cleanly.AttachmentComposer.Attachmentis what the tray holds: anid, akind(.imageor.file), thename,contentType(UTType),byteCount, the photo'sdataor the pickedfileURL, and a small JPEGthumbnail.Attachment.image(_:name:)builds one from photo bytes with its thumbnail, for your camera closure.Attachment.sampleis a PDF.onUploadreceives the attachment and a progress closure: callprogress(0.4)as bytes go, return when it's stored, throw to fail. A thrownLocalizedError's description is read by VoiceOver. Removing the tile cancels the task, so check for cancellation. Keep your own map fromattachment.idto the stored file.- Files hands you a security-scoped
fileURL: callstartAccessingSecurityScopedResource()before reading it inonUploadand stop after. onCamerapresents your camera and returnsAttachment.image(data), or nil when the person cancels. Without it, or on a device without a camera, the menu leaves Camera out.onSendreceivesAttachmentComposer.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; throwCancellationErroror letTask.sleepandURLSessiondo it. Any other error hands the message back and shows the error'serrorDescription, or "Couldn't send. Try again.", under the card.textis aBinding<String>for a draft your app keeps per conversation.characterLimitis 4,000 by default; nil removes the counter.maxAttachmentsis 6; the plus button dims at the limit.fileTypesis 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.
AttachmentComposerExampleat the end ofAttachmentComposer.swiftis 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. PasswrapsInNavigationStack: falseto push it in your own stack.
Theming
- Reads
SPThemefrom@Environment(\.spTheme)and requires thedesign-systemitem. - The card is
color.surfacewithradius.lgand a 1 pointseparatorhairline that turns totextPrimarywhile the field is focused. The field istype.bodyintextPrimarywith its prompt intextTertiary. - Plus is a 36 point
fillcircle with atextPrimarysymbol in a 44 point target. Send is the only accent: anaccentcircle with anaccentOnarrow,fillwith atextTertiaryarrow 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 andtextPrimaryon aseparatortrack on file chips, which sit onfill. Names aretype.subhead, detailstype.captionintextSecondary. Failures usedanger. - The counter is
type.captionwith monospaced digits; errors aretype.footnoteindanger. - State changes use
motion.smoothandmotion.snappy, and become short fades under Reduce Motion.
Requirements
NSCameraUsageDescriptiononly if youronCameraclosure presents the camera. Photos and Files need no Info.plist keys.
03
Keep building
Pieces that work well next to this one.