Engineering
A demo app is not a keyboard extension
A keyboard extension does not run where your demo app runs, and it does not receive touches the same way. Twice we shipped a fix that worked perfectly in a preview and did nothing at all in the real keyboard.
18 June 2026 · 3 min read · The PressboardKit team
A keyboard extension does not draw on screen the way an app does. UIInputViewController
hands its view to whatever app is being typed into, across a process boundary. The pixels
you see belong to Messages or Safari; the code that produced them is yours, somewhere else
entirely.
Most of the time that distinction is invisible. Twice it cost us days, both times because we had verified the behaviour in an in-process preview and believed it.
The overlay that received no touches
Fast typing dropped characters. Two fingers landing almost together were being reordered or discarded before our code saw the second one, which is a thing you can feel immediately and cannot reason your way out of.
The fix we wrote worked in the demo app’s live preview. It worked in an in-process harness test. On a device, in the real extension, it was completely dead — not slow, not intermittent: the code path was never entered once. A diagnostic overlay confirmed everything was mounted and correctly sized. The touches simply were not being delivered to it, and no amount of moving the surface up or down the view hierarchy changed that.
What we had run into is that the hosting arrangement an extension gets does not deliver touches the way an app does. There is a supported route through it, and the eventual fix was to stop fighting the framework and take that route instead. The hit-testing underneath did not have to change at all. Only where the touches came from did.
The gap between two keys that stayed dead
The second time was a dead zone. Tapping from the centre of one letter towards the centre of its neighbour, there was a visible band between the two keys where nothing was typed at all. The native keyboard has practically none — you cannot find the gap by trying.
Our hit-testing was not at fault: walked one point at a time, it resolved every position to the nearer key with no gaps. The problem was a level above it, in what was being offered a touch in the first place. We fixed that, confirmed it in the demo app, and moved on.
In the real extension the identical strip — exactly the width of the gap between two keys — stayed dead. The obvious remedies each closed the gap in our own process and did nothing out of it. What finally worked was something that looks, from the outside, indistinguishable from what had already failed. The difference only exists across a process boundary, and no in-process test can tell the two apart.
The same trap turned up later in the emoji panel, where a category tab responded only in a small island at its centre — users were tapping a flag three times to select it. Same cause, same fix.
Hence the harness
After that we stopped accepting the demo app as evidence for anything to do with touch.
There is now a UI test target that taps exact coordinates inside the keys’ actually rendered rectangles, read back from the running keyboard through the accessibility identifiers each key carries — not from a second, parallel copy of the engine’s own geometry that could quietly drift away from it. The same tests run in two places: in the demo app, and in the real, out-of-process extension, driven through another app’s text field.
One procedural note, which is the reason two of our dead-zone fixes looked plausible for as long as they did: a build system will happily run a stale extension binary while you believe you are testing the new one. Two of three candidate fixes were never actually tested against the build we thought we were testing. A clean uninstall before any A/B measurement is not optional.
Behaviour verified in an in-process preview is a hypothesis. The extension is the experiment.