ionic-team / ionic-team/capacitor-plugins

[@capacitor/keyboard] iOS: WebView frame permanently stuck at keyboard height after backgrounding (resize: native)

Open
#2,565 0 comments 0 reactions 0 assignees View on GitHub
platform: ios
Dominant language
Java
Stars
678
Forks
685
Avg merge
4d 22h
Merged PRs (30d)
3

Description

### Plugin(s)

`@capacitor/keyboard@8.0.5` (iOS)

### Description

With the default `resize: "native"` policy, the WKWebView frame can get permanently stuck at its keyboard-shrunk height after the app is backgrounded and returned to. The web content then occupies only the top of the screen, with a black band below it exactly one keyboard-height tall.

The band is **pure black** rather than `ios.backgroundColor`, because `CAPBridgeViewController.loadView()` does `view = webView` — shrinking the WebView shrinks the view controller's root view, exposing the bare `UIWindow`.

Measured on a 956pt-tall device: web content ends at exactly **611pt**, black band is **345pt**. That is precisely the `ResizeNative` arithmetic in `_updateFrame`:

```objc
webView.frame.height = window.height - origin.y - paddingBottom // 956 - 0 - 345 = 611
```

### Root cause

`setKeyboardHeight:delay:` assigns `paddingBottom` **synchronously**, but defers the frame update via `performSelector:afterDelay:`:

```objc
- (void)setKeyboardHeight:(int)height delay:(NSTimeInterval)delay
{
if (self.paddingBottom == height) {
return; // (2) guard makes the desync permanent
}
self.paddingBottom = height; // (1) state updated NOW
...
[weakSelf performSelector:@selector(_updateFrame) withObject:nil afterDelay:delay ...]; // frame updated LATER
}
```

Backgrounding with the keyboard up fires `keyboardWillHide`, which calls `setKeyboardHeight:0 delay:0.01`. If the app suspends inside that 10ms window, the run loop stops and `_updateFrame` is never delivered — leaving `paddingBottom == 0` while the frame is still short by the keyboard height.

It is then **unrecoverable**, because:

- `_updateFrame` is reachable from exactly one call site, `setKeyboardHeight:`
- `setKeyboardHeight:` early-returns whenever `paddingBottom` already equals the requested height — which it now always does for `0`
- `setResizeMode:` only flips the enum; it never touches the frame

So every subsequent `keyboardWillHide` is a no-op and the WebView stays short for the remaining life of the process. The guard compares the *intended* height against itself rather than against the *actual* frame, so the model believes it is already reconciled.

### Reproduction / evidence

Reproduced on an iPhone 17 Pro Max simulator (iOS 26, Xcode 26.6) by injecting the exact stuck state — frame short by 345pt while `paddingBottom` is 0 — and performing a real background/foreground cycle (verified same PID, i.e. a genuine resume rather than a cold start):

```
didBecomeActive paddingBottom=0 frame BEFORE={{0, 0}, {440, 611}}
```

The frame stays at 611 indefinitely; no keyboard event can repair it.

**User-level workaround:** focus any text input and dismiss the keyboard. That drives `paddingBottom` `0 → 345 → 0`, passing the guard in both directions, and the frame is restored.

### Expected behavior

The WebView frame should return to full height once the keyboard is gone, regardless of whether the app was suspended while the deferred update was pending.

### Suggested fix

Re-assert the frame when the app returns to the foreground. `_updateFrame` is already idempotent — it recomputes purely from the current window bounds and `paddingBottom` — so it is a no-op when state is consistent and a repair when a deferred update was dropped:

```objc
// in -load
[nc addObserver:self selector:@selector(onAppDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification object:nil];

- (void)onAppDidBecomeActive:(NSNotification *)notification
{
[self _updateFrame]; // deliberately not via setKeyboardHeight:, whose guard is the problem
}
```

Verified: with this in place the same background/foreground cycle restores the frame from 611 to 956, and the black band goes from 345pt to 0pt.

Alternatively (or additionally), the guard in `setKeyboardHeight:` could compare against the real `webView.frame.size.height` instead of the cached `paddingBottom`, so state can never silently diverge from the view.

### Unrelated minor bug spotted nearby

In `_updateFrame`, `f` is declared uninitialized:

```objc
CGRect f, wf = CGRectZero; // only `wf` is initialized
UIWindow *window = [self currentKeyWindow];
if (window) {
f = [window bounds];
}
...
[self.webView setFrame:CGRectMake(wf.origin.x, wf.origin.y, f.size.width - wf.origin.x, f.size.height - wf.origin.y - self.paddingBottom)];
```

If `currentKeyWindow` returns nil, `f` holds stack garbage which is then written straight into the WebView frame. Worth initializing to `CGRectZero` and bailing when there is no window to measure.

### Platforms

iOS

Contributor guide

Open the contributing guide

Research direction

Start in the iOS keyboard plugin implementation at -load, setKeyboardHeight:delay:, and _updateFrame; trace how the app-did-become-active path interacts with the cached paddingBottom. Reproduce the reported background/foreground cycle with resize: "native" and verify that the WebView returns to full height with no black band. Also inspect the currentKeyWindow nil case and the uninitialized frame noted in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, objective-c
Domain
mobile-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.