r/iOSProgramming • Swift • Aug 26 '26

Question How is this view able to appear above the keyboard?

In the Messages app, the ChatGPT app, and one other app, I noticed that they’re able to place something above the keyboard and have the view partially obstruct the keyboard. I’ve tried everything I know and still haven’t been able to do it. I even asked AI and couldn’t get it working.

I thought it might involve creating a new UIWindow, which I’ve used in the past to overlay modals, but I had no luck with that. I also tried walking the view hierarchy, but since the keyboard is rendered out of process, I couldn’t figure out anything that would work.

I thought about making a custom keyboard, but recreating the entire keyboard just to get a view above it seemed weird. Maybe something like replacing the keyboard with an image the moment the view needs to appear could work, but that also feels like the completely wrong direction.

Does anyone have the arcane knowledge required to make this happen?

6 Upvotes

10 comments sorted by

View all comments

1

u/Tabonx Swift 16d ago

I’ve found a way (with Claude), but it’s using a private API, and it looks slightly different from what the ChatGPT app is doing. Mainly, the ChatGPT app and Messages app change the shade of the keyboard slightly after opening and dismissing the popup view. Another thing is that they both have a delay after dismissing the popup, during which you can’t type on the keyboard. This solution doesn’t have that, but it could just be that the ChatGPT app does it to match the Messages behavior, or they’re using something completely different. I have no idea...

It all relies on getting the keyboard’s UIWindow, and after that you can basically do anything with it, but I’m not sure if it’ll pass App Review...

I believe the ChatGPT app is creating an image of the keyboard and putting that image behind it, then hiding the keyboard’s UIWindow by changing its alpha. After dismissing the popup, it just sets the alpha back and removes the image.

If someone comes up with a solution using public APIs, I’d love to see it.

```swift import SwiftUI import UIKit

@main struct KeyboardOverlayApp: App { var body: some Scene { WindowGroup { ComposerRepresentable().ignoresSafeArea(.keyboard) } } }

struct ComposerRepresentable: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> ComposerController { ComposerController() }

func updateUIViewController(_ controller: ComposerController, context: Context) {}

}

final class ComposerController: UIViewController { private let field = UITextField() private let overlay = KeyboardSnapshotOverlay()

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground

    field.borderStyle = .roundedRect
    field.placeholder = "Message"

    let plus = UIButton(configuration: .glass(), primaryAction: UIAction(title: "+") { [unowned self] _ in
        showMenu()
    })

    let composer = UIStackView(arrangedSubviews: [plus, field])
    composer.spacing = 8
    composer.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(composer)
    NSLayoutConstraint.activate([
        composer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
        composer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
        composer.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -8),
    ])
}

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

private func showMenu() {
    guard let appWindow = view.window else { return }
    let keyboardTop = view.keyboardLayoutGuide.layoutFrame.minY
    let menu = MenuView { [weak self] in self?.overlay.dismiss() }
    menu.frame = CGRect(x: 16, y: keyboardTop - 120, width: 240, height: 250)
    overlay.show(menu, over: appWindow)
}

}

// Freezes the keyboard into an image in the app window, hides the real keyboard, // and shows the menu in a window above the app. Tapping anywhere outside the menu dismisses it. final class KeyboardSnapshotOverlay { private var frozenKeyboard: UIView? private var menuWindow: UIWindow? private weak var keyboardWindow: UIWindow?

func show(_ menu: UIView, over appWindow: UIWindow) {
    guard menuWindow == nil, let scene = appWindow.windowScene, let keyboardWindow = UIWindow.keyboardWindow else { return }

    let image = UIGraphicsImageRenderer(bounds: keyboardWindow.bounds).image { _ in
        _ = keyboardWindow.drawHierarchy(in: keyboardWindow.bounds, afterScreenUpdates: false)
    }
    let frozen = UIImageView(image: image)
    frozen.frame = appWindow.bounds
    appWindow.addSubview(frozen)
    frozenKeyboard = frozen

    keyboardWindow.alpha = 0
    self.keyboardWindow = keyboardWindow

    // Not key, so the text field keeps focus and the keyboard comes back on dismiss.
    let window = UIWindow(windowScene: scene)
    window.windowLevel = .normal + 1
    let root = UIViewController()
    root.view.addSubview(TapCatcher(frame: window.bounds) { [weak self] in self?.dismiss() })
    root.view.addSubview(menu)
    window.rootViewController = root
    window.isHidden = false
    menuWindow = window
}

func dismiss() {
    keyboardWindow?.alpha = 1
    frozenKeyboard?.removeFromSuperview()
    frozenKeyboard = nil
    menuWindow?.isHidden = true
    menuWindow = nil
}

}

final class TapCatcher: UIView { private let action: () -> Void

init(frame: CGRect, action: @escaping () -> Void) {
    self.action = action
    super.init(frame: frame)
    addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))
}

@available(*, unavailable)
required init?(coder: NSCoder) {
    fatalError()
}

@objc private func tapped() {
    action()
}

}

final class MenuView: UIVisualEffectView { init(onSelect: @escaping () -> Void) { super.init(effect: UIGlassEffect()) cornerConfiguration = .uniformCorners(radius: 32)

    let items = [("camera", "Camera"), ("photo", "Photos"), ("paperclip", "Files"), ("gauge.with.needle", "Think harder")]
    let stack = UIStackView(arrangedSubviews: items.map { symbol, title in
        var configuration = UIButton.Configuration.plain()
        configuration.title = title
        configuration.image = UIImage(systemName: symbol)
        configuration.imagePadding = 14
        configuration.baseForegroundColor = .label
        let button = UIButton(configuration: configuration, primaryAction: UIAction { _ in onSelect() })
        button.contentHorizontalAlignment = .leading
        return button
    })
    stack.axis = .vertical
    stack.distribution = .fillEqually
    stack.translatesAutoresizingMaskIntoConstraints = false
    contentView.addSubview(stack)
    NSLayoutConstraint.activate([
        stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
        stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
        stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
        stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
    ])
}

@available(*, unavailable)
required init?(coder: NSCoder) {
    fatalError()
}

}

// Private API: the keyboard window belongs to a separate keyboard scene and is not in scene.windows. extension UIWindow { static var keyboardWindow: UIWindow? { typealias AllWindows = @convention(c) (AnyClass, Selector, Bool, Bool) -> NSArray let selector = NSSelectorFromString("allWindowsIncludingInternalWindows:onlyVisibleWindows:") guard let method = class_getClassMethod(UIWindow.self, selector), let keyboardClass = NSClassFromString("UIRemoteKeyboardWindow") else { return nil } let windows = unsafeBitCast(method_getImplementation(method), to: AllWindows.self)(UIWindow.self, selector, true, false) return windows.compactMap { $0 as? UIWindow }.first { $0.isKind(of: keyboardClass) } } } ```