Description
When KeyboardProvider is mounted in App.tsx (standard setup), iOS TextInputs with autoCapitalize="words", "sentences", or "characters" silently fall back to "none" on real devices. Simulator works correctly because its UIKit stub doesn't have the same delegate-coupled state machine.
Root cause
FocusedInputObserver.swift::substituteDelegate(_:) swaps the input view's delegate via DispatchQueue.main.async on textDidBeginEditingNotification. This swap clobbers iOS's autocapitalization state machine (which is keyed to the delegate identity at session start).
Trait-only props (keyboardType, textContentType, autoCorrect) survive because they're queried fresh each time. Only autocapitalizationType breaks because it's stateful — the trait says HOW to capitalize, the state machine decides WHEN, and the delegate swap resets the state machine.
Reproduction
- Mount
<KeyboardProvider> at app root
- Add a
<TextInput autoCapitalize="sentences" /> anywhere
- Run on a real iOS device (not simulator)
- Tap the field — keyboard never shifts to uppercase on first letter
Affected version
react-native-keyboard-controller@1.20.7, React Native 0.79.2, iOS 18 (tested on real iPhone)
Proposed fix
Patch substituteDelegate(_:) to save/restore autocapitalizationType around the swap, and call reloadInputViews() to rebuild the iOS input session:
private func substituteDelegate(_ input: UIResponder?) {
if let textField = input as? UITextField {
if !(textField.delegate is KCTextInputCompositeDelegate),
delegate.canSubstituteTextFieldDelegate(delegate: textField.delegate)
{
delegate.setTextFieldDelegate(delegate: textField.delegate, textField: textField)
let savedAutocap = textField.autocapitalizationType
textField.delegate = delegate
textField.autocapitalizationType = savedAutocap
if textField.isFirstResponder {
textField.reloadInputViews()
}
}
} else if let textView = input as? UITextView {
if !(textView.delegate is KCTextInputCompositeDelegate) {
delegate.setTextViewDelegate(delegate: textView.delegate)
let savedAutocap = textView.autocapitalizationType
textView.setForceDelegate(delegate)
textView.autocapitalizationType = savedAutocap
if textView.isFirstResponder {
textView.reloadInputViews()
}
}
}
}
Risk: LOW — uses public UIKit API only. No new attack surface. Behavior preserved when autocapitalizationType == .none.
Happy to open a PR if helpful.
Description
When
KeyboardProvideris mounted in App.tsx (standard setup), iOS TextInputs withautoCapitalize="words","sentences", or"characters"silently fall back to"none"on real devices. Simulator works correctly because its UIKit stub doesn't have the same delegate-coupled state machine.Root cause
FocusedInputObserver.swift::substituteDelegate(_:)swaps the input view's delegate viaDispatchQueue.main.asyncontextDidBeginEditingNotification. This swap clobbers iOS's autocapitalization state machine (which is keyed to the delegate identity at session start).Trait-only props (
keyboardType,textContentType,autoCorrect) survive because they're queried fresh each time. OnlyautocapitalizationTypebreaks because it's stateful — the trait says HOW to capitalize, the state machine decides WHEN, and the delegate swap resets the state machine.Reproduction
<KeyboardProvider>at app root<TextInput autoCapitalize="sentences" />anywhereAffected version
react-native-keyboard-controller@1.20.7, React Native0.79.2, iOS 18 (tested on real iPhone)Proposed fix
Patch
substituteDelegate(_:)to save/restoreautocapitalizationTypearound the swap, and callreloadInputViews()to rebuild the iOS input session:Risk: LOW — uses public UIKit API only. No new attack surface. Behavior preserved when
autocapitalizationType == .none.Happy to open a PR if helpful.