fix(chat): settle the send animation - #726
Merged
Merged
Conversation
A send collapses a multiline field, so `setBarHeight` runs at the same moment as the insertion and the settle-to-bottom. It was using `ChatMotion.swap`, whose bounce of 0.31 is the second-highest in the vocabulary, against a scroll spring set to 0.12 precisely so it wouldn't overshoot. Three curves overshooting the same pixels by different amounts is what read as the bar and the transcript coming apart. It now uses `keyboardScroll` — the transcript following bottom chrome, zero bounce — which is the job it is actually doing. The other half was the inset. `setBottomInset` refuses to write mid-batch-update, which is right on its own, but it was dropping the request rather than holding it: a bar height that landed inside the update left the bar animating to a height the transcript only matched on some later layout pass, as a snap. It now parks in `pendingBottomInset` — the slot the context-menu freeze already used — and the batch update's completion applies it.
Two things the edit work added that a plain send now pays for. `ComposerModel.clear()` wrote `mode = .new` unconditionally, and `@Observable` fires its registrar on assignment without comparing. The chat screen's body reads `isEditing` and `editingStableID`, both derived from `mode`, so every send rebuilt the whole screen — toolbar included — and re-ran `updateUIViewController` at the frame the insertion started. Before the edit work, clearing the draft touched only the composer subtree. Guarded, so the write happens when the value actually changes. The bar's row went top-aligned, which made the leading control track the field's top edge and travel with every line the draft gained or lost. Nothing animates that travel: the bar's springs key on `chatExists` and `isEditing`, neither of which moves during a send, so the control snapped down while the bar's height sprang underneath it. Back to bottom-aligned, against the bar's own pinned bottom.
The column pinned one horizontal edge and let the other float, so its width was its widest arranged subview. For a short message that was the receipt line, not the bubble: clearing the receipt shrank the column by 40pt and slid its origin the same distance. The bubble looked still only because its offset inside the stack moved the opposite way by exactly as much. That cancellation holds on final values. Mid-flight it is three separate layer animations — stack bounds, stack position, bubble position — and any one off the others' curve slides the bubble sideways, which is the leftward shift on the row above an insertion. Pin both edges and let the stack's alignment hug the sender's side. The stack measures (12, 0, 378, h) with and without a receipt, and the bubble's absolute x is unchanged at 347.67, so only the receipt row animates. Self-sizing needs an explicit fit now: a full-width column makes UIKit's default compressed horizontal measurement report the content's width, which ChatLayout discards anyway for a .fullWidth row. Measure height at the width the layout asked for instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four regressions in the send animation, all introduced alongside the chat edit work.
The bar's height change fought the send. A send collapses a multiline field, so
setBarHeightruns at the same moment as the insertion and the settle-to-bottom. It usedChatMotion.swap(bounce 0.31) against a scroll spring set to 0.12 precisely so it wouldn't overshoot. It now useskeyboardScroll— zero bounce, the transcript following bottom chrome, which is the job it is doing.The bottom inset was dropped rather than deferred.
setBottomInsetrefuses to write mid-batch-update, but it discarded the request instead of holding it, so a bar height landing inside the update left the transcript matching it a layout pass later, as a snap. It now parks inpendingBottomInsetand the batch update's completion applies it.A send rebuilt the whole screen.
ComposerModel.clear()wrotemode = .newunconditionally and@Observablefires on assignment without comparing. The chat screen's body readsisEditingandeditingStableID, both derived frommode, so every send re-ranupdateUIViewControllerat the frame the insertion started. Guarded to write only on a real change.The bar's leading control tracked the field's top edge. Top-aligning the bar's row made it travel with every line the draft gained or lost, unanimated — the bar's springs key on
chatExistsandisEditing, neither of which moves during a send. Back to bottom-aligned.The row above an insertion slid left.
ChatColumnCellpinned one horizontal edge and let the other float, so the column's width was its widest arranged subview — for a short message, the receipt line rather than the bubble. Clearing the receipt shrank the column by 40pt and slid its origin the same distance; the bubble only looked still because its offset inside the stack moved the opposite way by exactly as much. That cancellation holds on final values, but mid-flight it is three separate layer animations that have to share a curve.Both edges are pinned now and the stack's
alignmenthugs the sender's side. Measured at a 402pt row, the stack is(12, 0, 378, h)with and without a receipt, the own bubble's absolute x is unchanged at 347.67, other-sender rows still hug leading at 12, and long text still wraps at the cap. Only the receipt row animates. A full-width column also makes UIKit's default compressed horizontal measurement report the content's width, so the cell now measures height explicitly at the width the layout asked for — a widthChatLayoutforces to full for a.fullWidthrow anyway.