Environment
- react-native-keyboard-controller: 1.21.5
- react-native: 0.81.5 (Expo SDK 54, new architecture)
- react-native-reanimated: 4.3.2
- iOS Simulator: iPhone 17 Pro, iOS 26.5
Symptom
With a multiline, auto-growing TextInput inside KeyboardAwareScrollView, the initial focus reveal works (caret-accurate), but the view never follows typing: once text wraps to a new line, the caret sinks toward/behind the keyboard and no re-scroll happens.
Diagnosis (instrumented KASV in-place with HTTP probes)
-
onSelectionChange handlers never fire during typing. Across dozens of keystrokes (controlled and uncontrolled inputs alike), the only selection event that ever reaches useFocusedInputHandler is the one at focus — which comes from FocusedInputObserver's direct updateSelectionPosition call on focusDidSet, not from the delegate. We saw no evidence the delegate-intercepted lane (textViewDidChangeSelection) fires at all.
-
Suspected root cause: UITextView+DelegateManager.m's setForceDelegate: writes the private _delegate ivar via class_getInstanceVariable/object_setIvar. On the iOS 26 runtime this appears to silently no-op (ivar lookup fails or UIKit no longer routes through it), so the composite delegate is never actually installed.
-
Knock-on in KASV: the KVO-driven growth lane (useAnimatedReaction on input.value.layout.height) does fire on every wrap, but updateLayoutFromSelection() computes from lastSelection, which stays frozen at its focus-time value — so the recomputed scroll is exactly 0 every time. Probe trace at the moment a line wrapped:
uls-ok-y259-h287.33-absY229 <- stored caret y still 259 (focus-time), input now 287 tall
perform-scroll-newPos603-moved0-kb335
Workaround we're shipping (patch-package, JS-only)
In the growth useAnimatedReaction, floor the stored caret at the input's new bottom edge before scrollFromCurrentPosition() — growth while composing means the caret rides the content bottom, and on platforms where selection events work the value clamps to the same bottom edge anyway:
if (lastSelection.value) {
lastSelection.value = {
...lastSelection.value,
selection: {
...lastSelection.value.selection,
end: {
...lastSelection.value.selection.end,
y: Math.max(lastSelection.value.selection.end.y, current.layout.height),
},
},
};
}
scrollFromCurrentPosition();
Verified on the same setup: the wrap now produces perform-scroll … moved28 and the input bottom stays pinned at visibleRect - bottomOffset through repeated wraps.
This is a mitigation, not the fix — the delegate installation itself needs an iOS-26-safe mechanism. Happy to test candidate fixes on this setup.
Environment
Symptom
With a multiline, auto-growing
TextInputinsideKeyboardAwareScrollView, the initial focus reveal works (caret-accurate), but the view never follows typing: once text wraps to a new line, the caret sinks toward/behind the keyboard and no re-scroll happens.Diagnosis (instrumented KASV in-place with HTTP probes)
onSelectionChangehandlers never fire during typing. Across dozens of keystrokes (controlled and uncontrolled inputs alike), the only selection event that ever reachesuseFocusedInputHandleris the one at focus — which comes fromFocusedInputObserver's directupdateSelectionPositioncall on focusDidSet, not from the delegate. We saw no evidence the delegate-intercepted lane (textViewDidChangeSelection) fires at all.Suspected root cause:
UITextView+DelegateManager.m'ssetForceDelegate:writes the private_delegateivar viaclass_getInstanceVariable/object_setIvar. On the iOS 26 runtime this appears to silently no-op (ivar lookup fails or UIKit no longer routes through it), so the composite delegate is never actually installed.Knock-on in KASV: the KVO-driven growth lane (
useAnimatedReactiononinput.value.layout.height) does fire on every wrap, butupdateLayoutFromSelection()computes fromlastSelection, which stays frozen at its focus-time value — so the recomputed scroll is exactly 0 every time. Probe trace at the moment a line wrapped:Workaround we're shipping (patch-package, JS-only)
In the growth
useAnimatedReaction, floor the stored caret at the input's new bottom edge beforescrollFromCurrentPosition()— growth while composing means the caret rides the content bottom, and on platforms where selection events work the value clamps to the same bottom edge anyway:Verified on the same setup: the wrap now produces
perform-scroll … moved28and the input bottom stays pinned atvisibleRect - bottomOffsetthrough repeated wraps.This is a mitigation, not the fix — the delegate installation itself needs an iOS-26-safe mechanism. Happy to test candidate fixes on this setup.