SwiftUI Sheet + Keyboard Choreography
Present a sheet with the keyboard rising as one motion (iMessage-compose style), and dismiss them together — in pure-ish SwiftUI.
Why naive approaches fail (all observed, not hypothetical)
| Attempt | What actually happens |
|---|---|
draftFocused = true after a delay (300–500ms) |
Works, but visibly staggered: sheet settles, THEN keyboard pops and re-lifts the input bar — an up-then-down bounce |
draftFocused = true immediately in .task/.onAppear |
@FocusState cannot co-animate with a presentation; worse, at bind time the field may not be mounted, so focus fails silently — no keyboard at all |
Any auto-focus on a .medium detent sheet |
The keyboard SHOVES the medium sheet to full height (Apple Forums #743274): the input bar travels sheet-distance + keyboard-height and settles back — the big ride |
| Retiring the prewarm field when you attempt the handoff | If focus didn't land yet, unmounting the prewarm resigns first responder and the keyboard drops mid-presentation |
The pattern (4 pieces, all required)
- UIKit prewarm field — a near-invisible
UITextFieldthat callsbecomeFirstResponder()indidMoveToWindow. UIKit can grab the keyboard during a presentation transition, so the keyboard rises with the sheet. (Pattern origin: github.com/naan/FocusOnAppear.) - Land-gated handoff — after the sheet content mounts (~400ms is safe; the keyboard is already visibly up so the wait is invisible), set the real field's
@FocusState. Retire the prewarm only when focus actually lands (onChange(of: focused)), never on the attempt. - Open at
.large— never auto-focus on a.mediumdetent. If the sheet has a detent-selection binding, reset it to.largefor the next open. - Freeze bottom accessories from frame one — anything that collapses when focus lands (emote strips, suggestion rows) must render collapsed from the first frame during auto-open, or its height change adds another bar jump mid-rise.
Exit symmetry: an explicit close control clears focus and dismisses in the same transaction (focused = false; onClose()); sheet drag-dismiss already pulls the keyboard along natively.
Skeleton
Complete adaptable implementation in references/implementation.md — the prewarm representable, state wiring, handoff task, and accessory gating, extracted from a shipped app.
Red flags
- A bare
Task.sleepbeforefocused = truewith no prewarm → you have the gap autoFocusPending = false(or prewarm unmount) in the same statement as the focus attempt → keyboard will drop when the attempt races the mountpresentationDetents([.medium, .large])on an auto-focusing sheet without forcing.largeat open → overshoot