[PM-28227] fix: Keep keyboard alive in autofillText mode via SwiftUI @FocusState anchor#2599
[PM-28227] fix: Keep keyboard alive in autofillText mode via SwiftUI @FocusState anchor#2599fedemkr wants to merge 4 commits into
Conversation
…locking, but not on search.
…chor in a workaround. Replace the UIKit `KeyboardAnchorTextField.becomeFirstResponder()` call on Cancel with a hidden SwiftUI `TextField` bound to a `@FocusState` enum. UIKit's explicit `becomeFirstResponder()` produces `fromBecomeFirstResponder:1` → `delayEndInputSession:YES`, starting a ~5-second RTI countdown that dismisses the extension. SwiftUI's focus machinery uses `fromBecomeFirstResponder:0` → `delayEndInputSession:NO`, keeping the keyboard panel alive indefinitely.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2599 +/- ##
==========================================
- Coverage 87.20% 87.15% -0.06%
==========================================
Files 1894 1895 +1
Lines 167519 167838 +319
==========================================
+ Hits 146087 146279 +192
- Misses 21432 21559 +127 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| /// `useKeyboard` is computed as `allowsSystemInputView && !hasCustomInputView && responderRequiresKeyboard`. | ||
| /// Any non-nil `inputView` makes `!hasCustomInputView = 0` → `useKeyboard = 0` → `delayEndInputSession:YES` | ||
| /// on every delegate transition, firing `endRemoteTextInputSessionWithID` after ~5 seconds. |
There was a problem hiding this comment.
❓ Is this still a valid comment? I don't see useKeyboard declared anywhere? Some of the comments throughout this PR seem overly verbose, do you think these are all helpful for future context?
| // Create the anchor directly here — `self.context` is nil at this point so | ||
| // `installKeyboardAnchor`'s context guard would return early. This is the only | ||
| // synchronous call site where the view is in the window, no children exist, and | ||
| // nothing else can steal focus. `initializeApp` launches an async Task, so anchoring | ||
| // inside it would be too late. | ||
| let anchor = KeyboardAnchorTextField(frame: .zero) | ||
| view.addSubview(anchor) | ||
| keyboardAnchor = anchor | ||
| anchor.becomeFirstResponder() |
There was a problem hiding this comment.
🤔 This is very similar to installKeyboardAnchor(). I see why it's duplicated from the comment, but I wonder if we could simplify by:
- Keep this logic here, which creates and installs
KeyboardAnchorTextField. - In
show(child:)the keyboard anchor should already exist at that point if you're in the autofill text context. So could we replaceinstallKeyboardAnchor()withkeyboardAnchor?.becomeFirstResponder()which will only cause it to become first responder if it has already been created in this flow here? - Remove
installKeyboardAnchor().
| // Safety net: reclaim anchor when keyboard starts hiding and anchor isn't FR. | ||
| NotificationCenter.default.addObserver( | ||
| self, | ||
| selector: #selector(keyboardWillHide), | ||
| name: UIResponder.keyboardWillHideNotification, | ||
| object: nil, | ||
| ) |
There was a problem hiding this comment.
🤔 Could we put this and keyboardWillHide() in KeyboardAnchorTextField? That would help keep this isolated to KeyboardAnchorTextField and hopefully easier to remove if the root issue is fixed.
🎟️ Tracking
PM-28227
📔 Objective
Fixes the
autofillTextmode keyboard being dismissed (and the extension closing ~5 seconds later) when the user cancels an inline vault search.In
autofillTextmode the extension is shown as a keyboard panel (InputUI) backed by a Remote Text Input (RTI) session. Any text field that becomes first responder via UIKit's explicitbecomeFirstResponder()causes the RTI system to tag the session withdelayEndInputSession:YES(fromBecomeFirstResponder:1), starting a ~5-second countdown that tears down the keyboard panel and dismisses the extension when it fires.The previous approach used a UIKit
KeyboardAnchorTextFieldand calledbecomeFirstResponder()on it when the search Cancel button was tapped — triggering exactly that timer. This PR replaces that path with a hidden SwiftUITextFieldbound to a@FocusStateenum (FocusableField.anchor / .search). SwiftUI's internal focus machinery usesfromBecomeFirstResponder:0→delayEndInputSession:NO, so every focus handoff (including Cancel → anchor) is a clean RTI session continuation with no countdown timer.As a secondary fix,
.searchable/UISearchControlleris replaced with a custom inline search bar inautofillTextmode, sinceUISearchControlleralso calls explicitbecomeFirstResponder()on resign.📸 Screenshots
N/A — keyboard extension panel behaviour, no visual UI change.