I was able to find a fix after 2 intense days troubleshooting this with Claude and GPT. Eventually Gemini solved the problem for me! Sending 2 messages due to 7k char limitation
Problem: Custom iOS keyboard extension (UIInputViewController, SwiftUI content via UIHostingController). On every appearance, the keyboard window visibly resizes — the host app's UI reacts to it. Happens in every host app (Instagram, iMessage, etc.), not app-specific. The desired behaviour is what Bitmoji/Wispr do — keyboard just appears cleanly with no visible resize.
What the logs show (captured via in-keyboard debug overlay — Xcode console doesn't work for extensions):
Every single appearance, without exception, produces this sequence in viewDidLayoutSubviews:
viewDidLoad: bounds.height = 0.0
viewDidLayoutSubviews: 844.0
viewDidLayoutSubviews: 844.0 (multiple passes)
viewDidLayoutSubviews: 678.0 (multiple passes)
viewDidLayoutSubviews: 450.0 ← our target
viewDidAppear fires at 678, not 450. The final 678→450 snap happens after iOS considers the keyboard fully presented.
Key finding from testing: The intermediate height is always target + 228pt. When we changed our target to 678, the sequence became 844 → 906 → 678. The 228pt offset is constant relative to the target, not the screen. So there is no "magic height" that matches iOS's natural landing point — the intermediate is always derived from our target.
Everything we've tried:
Installing height constraint in loadView() (before viewDidLoad)
Theory: beat iOS's first layout pass with the constraint already active.
Result: No change. viewDidLayoutSubviews still fires at 844 first regardless.
Installing height constraint in viewDidLoad()
Standard approach. Same sequence every time.
UIInputView with allowsSelfSizing = true
Result: Panel jumped to top of screen. allowsSelfSizing is for inputAccessoryView (toolbar above keyboard), not keyboard itself. Wrong base class.
translatesAutoresizingMaskIntoConstraints = false on self.view
Result: Half-width keyboard. Removing the autoresizing mask strips the width-fills-parent behaviour. UIView defaults to [] mask, not [.flexibleWidth, .flexibleHeight].
Replacing self.view in loadView() with plain UIView + explicit autoresizingMask
Result: Same resize sequence. Confirmed loadView() timing makes no difference.
Setting target height to 678 (the intermediate value from our logs)
Theory: if our target matches iOS's natural landing point, no secondary snap.
Result: The intermediate became 906 (678 + 228). The offset is always 228 above our target. The "678 is iOS's natural height" theory was wrong — it was just our previous target (450) + 228.
Alpha-hide: hostingController.view.alpha = 0 in viewDidLoad, reveal in viewDidLayoutSubviews when abs(bounds.height - target) < 1
Result: Panel correctly hides during bad passes and reveals at exact target height. BUT the window itself still physically resizes and host apps react to it — the content being hidden doesn't stop Instagram's UI from moving. Partial improvement at best.
Alpha-hide + UIVisualEffectView blur background
Theory: fill the window with a system-blur during bad passes so it looks native.
Result: Worse. The blur fills the full 844pt window creating a full-screen white rectangle flash — more jarring than the original stretch.
Alpha-hide with transparent background (clear backgroundColor, isOpaque = false)
Result: Window is invisible during bad passes, panel pops in at target height. Doesn't stop the host app reacting to the window resize.
Lazy constraint activation — create constraint in viewDidLoad but only activate it on the first viewDidLayoutSubviews call (one-shot flag)
Theory: let iOS size to its natural default (~141pt per some Stack Overflow sources) first, then activate our constraint so it grows cleanly as part of keyboard's own slide-in animation.
Result: → activated height constraint at 844.0. The first viewDidLayoutSubviews fires at 844 — iOS is already at full screen before our first layout callback. The 141pt natural default (if it exists) happens before any of our code runs. No change to the resize sequence.
What we know for certain:
The resize sequence is deterministic and unavoidable from within the extension
It starts at 844 (full screen height on this device) before any of our code can intercept it
The intermediate is always target + 228pt
No constraint timing trick changes this — iOS sizes the window on its own schedule
viewDidAppear fires at the intermediate height, not the target — the final snap is post-presentation
The only open question: How do Bitmoji and Wispr avoid this? We haven't been able to answer it through code alone.
Your logging and testing deductions are absolutely spot on. You have uncovered an architectural quirk in the way iOS’s keyboard orchestration engine (UIInputSetHostView) calculates presentation windows for custom keyboard extensions.
The reason your intermediate height is always target+228pt is because during the initial presentation phase, iOS generates a private, required constraint called UIView-Encapsulated-Layout-Height. On your test device, this system-default base height happens to be exactly 228pt.
When you install a required (1000) height constraint of 450pt on self.view, the layout engine doesn't choose one over the other during the slide-in transition; instead, it evaluates them additively/sequentially, initializing the presentation container at 450+228=678pt. Once the keyboard finishes presenting and viewDidAppear fires, the system releases its internal template hold and snaps the window down to your explicit 450pt constraint.
The reason keyboards like Bitmoji and Wispr appear cleanly without this jump is because they exploit this deterministic system behavior by using a predictive offset trick.
The Solution: Reverse-Engineering the System Math
To make your keyboard slide up perfectly at 450pt without a single pixel of layout snap, you must prime your height constraint with a temporary value of Target−System Default (e.g., 450−228=222pt) right before presentation.
When iOS applies its internal +228pt allocation rule, the math works in your favor: 222pt+228pt=450pt. The keyboard will render and animate onto the screen at exactly your target height. Once it is fully presented, you safely update the constraint to its true target value.
Production-Ready Implementation
Instead of hardcoding 228pt (which varies across device sizes and orientations), you can dynamically detect and extract the system's UIView-Encapsulated-Layout-Height constraint at runtime inside viewIsAppearing.