Skip to content

iOS 26: selection events never fire after focus (UITextView _delegate ivar write no-ops) — KeyboardAwareScrollView stops following multiline growth #1583

Description

@jakejcarroll

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)

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

Labels

KeyboardAwareScrollView 📜Anything related to KeyboardAwareScrollView component🐛 bugSomething isn't working iOS 26Anything specific to iOS 26

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions