Custom Keyboard help

import UIKit

class KeyboardViewController: UIInputViewController {

// MARK: - Properties
private var keyboardView: KeyboardView!
private var heightConstraint: NSLayoutConstraint!
private var hasInitialLayout = false

// 存储系统键盘高度和动画参数
private var systemKeyboardHeight: CGFloat = 300
private var keyboardAnimationDuration: Double = 0.25
private var keyboardAnimationCurve: UIView.AnimationOptions = .curveEaseInOut

// MARK: - Lifecycle
override func viewDidLoad() {
    super.viewDidLoad()
    setupKeyboard()
    
    
}



override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    // 在视图显示前更新键盘高度,避免闪动
    if !hasInitialLayout {
        hasInitialLayout = true
    }
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
}

// MARK: - Setup
private func setupKeyboard() {
    // 创建键盘视图
    keyboardView = KeyboardView()
    keyboardView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(keyboardView)
    
    // 设置约束 - 确保键盘贴紧屏幕底部
    NSLayoutConstraint.activate([
        keyboardView.leftAnchor.constraint(equalTo: view.leftAnchor),
        keyboardView.rightAnchor.constraint(equalTo: view.rightAnchor),
        keyboardView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
    ])
    
    // 设置初始高度约束(使用系统键盘高度或默认值)
    let initialHeight = systemKeyboardHeight
    heightConstraint = keyboardView.heightAnchor.constraint(equalToConstant: initialHeight)
    heightConstraint.isActive = true
}





// MARK: - Layout Events
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
}

override func viewSafeAreaInsetsDidChange() {
    super.viewSafeAreaInsetsDidChange()
}

// MARK: - 键盘高度请求
// 这个方法可以确保键盘扩展报告正确的高度给系统
override func updateViewConstraints() {
    super.updateViewConstraints()
    
    // 确保我们的高度约束是最新的
    if heightConstraint == nil {
        let height = systemKeyboardHeight > 0 ? systemKeyboardHeight : 216
        heightConstraint = NSLayoutConstraint(
            item: self.view!,
            attribute: .height,
            relatedBy: .equal,
            toItem: nil,
            attribute: .notAnAttribute,
            multiplier: 0.0,
            constant: height
        )
        heightConstraint.priority = UILayoutPriority(999)
        view.addConstraint(heightConstraint)
    } else {
        let height = systemKeyboardHeight > 0 ? systemKeyboardHeight : 216
        heightConstraint.constant = height
    }
}

}

// MARK: - Keyboard View Implementation class KeyboardView: UIView {

private var keysContainer: UIStackView!

override init(frame: CGRect) {
    super.init(frame: frame)
    setupView()
}

required init?(coder: NSCoder) {
    super.init(coder: coder)
    setupView()
}

private func setupView() {
    backgroundColor = UIColor(red: 0.82, green: 0.84, blue: 0.86, alpha: 1.0)
    
    // 创建按键容器
    keysContainer = UIStackView()
    keysContainer.axis = .vertical
    keysContainer.distribution = .fillEqually
    keysContainer.spacing = 8
    keysContainer.translatesAutoresizingMaskIntoConstraints = false
    addSubview(keysContainer)
    
    // 添加约束 - 确保内容在安全区域内
    NSLayoutConstraint.activate([
        keysContainer.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 8),
        keysContainer.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: 8),
        keysContainer.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -8),
        keysContainer.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8)
    ])
    
    // 添加键盘行
}

}

I don't see you described what doesn't work... For folks to help, you might consider providing more context about your question, for example, what the issue is, and how you trigger the issue. See tips on writing forums posts, if necessary.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

The above is a very simple code. When I switch from the system keyboard to the extended keyboard, the extended keyboard will flash. The first switch will not flash, and the subsequent keyboard switches will flash. This is more obvious on phones with a safe area at the bottom of the screen. Thank you.

I play with an iOS app + a custom keyboard extension created with the project templates Xcode provides, without adding any code, on my iOS 26 device, and do see that when switching to the custom keyboard, the keyboard has a flash, which seems to be triggered because the system gives the keyboard a bigger initial height, and then change it after the keyboard appears on the screen.

I unfortunately don’t see anything that can fix the issue. A keyboard extension runs in a separate process. The system uses an internal mechanism to render the keyboard with the hosting iOS app. As a result, the extension can only change the height of the keyboard AFTER its primary view initially draws on screen. There is no way to avoid the flash by adjusting the height of the keyboard before it appears on screen.

Based on that, I can only suggest that you file a feedback report for the relevant team to investigate – If you do so, please share your report ID.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

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:

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

  1. Installing height constraint in viewDidLoad()

Standard approach. Same sequence every time.

  1. UIInputView with allowsSelfSizing = true

Result: Panel jumped to top of screen. allowsSelfSizing is for inputAccessoryView (toolbar above keyboard), not keyboard itself. Wrong base class.

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

  1. Replacing self.view in loadView() with plain UIView + explicit autoresizingMask

Result: Same resize sequence. Confirmed loadView() timing makes no difference.

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

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

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

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

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

import UIKit

import SwiftUI

class KeyboardViewController: UIInputViewController {

private var customHeightConstraint: NSLayoutConstraint?

private let targetHeight: CGFloat = 450.0

private var systemDefaultHeight: CGFloat = 228.0 // Safe fallback



override func viewDidLoad() {

    super.viewDidLoad()

    setupSwiftUIContent()

    

    // Initialize the constraint at default high priority to avoid layout warnings

    customHeightConstraint = view.heightAnchor.constraint(equalToConstant: targetHeight)

    customHeightConstraint?.priority = UILayoutPriority(999)

    customHeightConstraint?.isActive = true

}



override func viewIsAppearing(_ animated: Bool) {

    super.viewIsAppearing(animated)

    

    // Dynamically find iOS's system-encapsulated height constraint

    if let encapsulatedConstraint = view.constraints.first(where: { 

        $0.firstItem === view && $0.firstAttribute == .height && $0 !== customHeightConstraint 

    }) {

        systemDefaultHeight = encapsulatedConstraint.constant

    }

    

    // Apply the offset trick: target (450) - system default (228) = 222

    // iOS will add 228 during presentation, landing flawlessly at 450

    customHeightConstraint?.constant = targetHeight - systemDefaultHeight

}



override func viewDidAppear(_ animated: Bool) {

    super.viewDidAppear(animated)

    

    // Presentation is complete. Update to the true target height.

    // Because the window is already physically at 450, this creates zero visual jump.

    customHeightConstraint?.constant = targetHeight

}



private func setupSwiftUIContent() {

    let keyboardView = YourSwiftUIKeyboardView()

    let hostingController = UIHostingController(rootView: keyboardView)

    

    addChild(hostingController)

    view.addSubview(hostingController.view)

    hostingController.view.translatesAutoresizingMaskIntoConstraints = false

    

    // Pin hosting controller edges tightly to self.view

    NSLayoutConstraint.activate([

        hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),

        hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),

        hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),

        hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)

    ])

    

    hostingController.didMove(toParent: self)

}

}

Custom Keyboard help
 
 
Q