[Masterclass] Re-pin a bottom-anchored scroll view when the keyboard hides - #50
Merged
Merged
Conversation
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>
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.
Fixes NativePHP/mobile-air#316. cc @shrutibalasawebdev
Demo:
/masterclass/chat-repinin the super-native demo app — the issue's exact layout. Verified on device by @shanerbaner82.Cause
NativeUIScrollViewRendererobservedkeyboardWillShowNotificationand 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
keyboardWillHideNotificationtoo, 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@ExperimentalLayoutApiopt-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 thatlistState.layoutInforead 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