Skip to content

[Masterclass] Re-pin a bottom-anchored scroll view when the keyboard hides - #50

Merged
shanerbaner82 merged 1 commit into
mainfrom
fix/masterclass-316-repin-on-keyboard-hide
Aug 11, 2026
Merged

[Masterclass] Re-pin a bottom-anchored scroll view when the keyboard hides#50
shanerbaner82 merged 1 commit into
mainfrom
fix/masterclass-316-repin-on-keyboard-hide

Conversation

@shanerbaner82

Copy link
Copy Markdown
Contributor

Fixes NativePHP/mobile-air#316. cc @shrutibalasawebdev

Demo: /masterclass/chat-repin in the super-native demo app — the issue's exact layout. Verified on device by @shanerbaner82.


Cause

NativeUIScrollViewRenderer observed keyboardWillShowNotification and nothing else.

The keyboard resizes the scroll viewport in both directions — it shrinks on the way in (the screen shifts up for keyboard avoidance) and grows back on the way out. Handling only the show left the list stranded mid-screen with empty space beneath it once the keyboard closed.

Which is exactly how @shrutibalasawebdev described it: "only the first half works." Half the bug was invisible precisely because the half that worked looked correct.

Fix

Observe keyboardWillHideNotification too, sharing the show handler's animation so both transitions travel in step with the keyboard (same one-runloop defer, same keyboard-reported duration).

The dismiss is guarded, and the guard is the point.

scrollDismissesKeyboard(.interactively) means dragging the list toward older messages is how you dismiss the keyboard. Re-pinning unconditionally would yank the reader straight back to the bottom and undo the very scroll that dismissed it — trading this bug for a worse one.

So the hide handler only re-pins when the list was still actually sitting at the bottom, tracked by whether the existing 1pt bottom anchor is on screen (onAppear / onDisappear). The anchor being visible is the definition of "still stuck to the bottom", so no new machinery was needed.

The demo screen tests all three cases in order: re-pin on show, re-pin on hide, and drag-up-to-dismiss staying put.

Android is deliberately not covered

Its LaunchedEffect(stickBottom, contentSignal) only fires on content change — nothing observes IME visibility, so it never re-pins for the keyboard in either direction. Same bug, arguably worse than iOS's.

I stopped short rather than guess. It needs WindowInsets.isImeVisible (an @ExperimentalLayoutApi opt-in not used anywhere in either repo yet), plus an equivalent at-bottom guard — and Android dismisses on drag too (detectVerticalDragGestures { keyboardController?.hide() }), so it needs one. The problem is that listState.layoutInfo read after an IME change already reflects the resized viewport, so every version I sketched either mis-fires on show or on drag-dismiss. That wants a device to get right.

Worth filing as its own issue.

Testing

Full suite green: 217 passed, Pint clean. No PHP surface changed — this is a SwiftUI observer fix, verified on device.

🤖 Generated with Claude Code

The renderer observed `keyboardWillShowNotification` and nothing else. The
keyboard resizes the scroll viewport in BOTH directions — it shrinks on the
way in (the screen shifts up for keyboard avoidance) and grows back on the
way out — so handling only the show left the list stranded mid-screen with
empty space beneath it once the keyboard closed. Half the bug was invisible
because the half that worked looked correct.

Observe `keyboardWillHideNotification` too, sharing the show handler's
animation so both transitions travel in step with the keyboard.

The dismiss is guarded, and the guard is the point.
`scrollDismissesKeyboard(.interactively)` means dragging the list toward
older messages is itself how you dismiss the keyboard, so re-pinning
unconditionally would yank the reader straight back to the bottom and undo
the scroll that dismissed it — trading this bug for a worse one. The hide
handler therefore only re-pins when the list was still sitting at the bottom,
tracked by whether the existing bottom anchor is on screen.

Android is NOT covered here. Its `LaunchedEffect(stickBottom, contentSignal)`
only fires on content change, so it never re-pins for the keyboard in either
direction — the same bug, arguably worse. Fixing it needs `WindowInsets`
`isImeVisible` (an ExperimentalLayoutApi opt-in used nowhere in either repo
yet) plus an equivalent at-bottom guard, and `listState.layoutInfo` read after
an IME change already reflects the resized viewport. Filed separately rather
than guessed at.

Fixes NativePHP/mobile-air#316. Native changes are compile-unverified by me;
verified on device by @shanerbaner82.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shanerbaner82
shanerbaner82 merged commit 130bdc8 into main Aug 11, 2026
3 checks passed
@shanerbaner82
shanerbaner82 deleted the fix/masterclass-316-repin-on-keyboard-hide branch August 11, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Masterclass] Bottom-pinned scroll view does not re-pin when the keyboard hides

1 participant