Skip to content
PressboardKit

Features

Everything the native keyboard does, and a switch for every bit of it.

PressboardKit is not a keyboard that resembles the iOS one. Its look, its timing and the way it responds to a thumb were measured off the system keyboard in light and dark, and where we diverge from native we say so on this page.

73
Languages, layouts and callouts included
51
Configurable properties on KeyboardBehavior
466
Tests, run on the simulator
iOS 26
Minimum deployment target

Native parity

The engine's one hard requirement: a user must not be able to tell it apart from the keyboard Apple ships. Where the two differ, that is a defect, not a design choice.

The default look is the system keyboard's

Measured

Key fills, key shape, the shadow under a key, the typeface and the proportions of the rows come off the system keyboard in both light and dark, and the default style reproduces them. Nothing here was eyeballed from a screenshot at the wrong scale, and every value is a named constant with a test behind it.

The key-pop is the real one

characterPreviewStyleMeasured

Not the bubble most reimplementations draw. The real one is a detached square that floats above the key you are holding, the same size whichever key that is, and the pressed key stays visible underneath with its letter still on it. Several details of it are the opposite of what they look like until you measure them on a device, which is what we did.

Pressed keys without a pop still change colour

Measured

Backspace, return, space, emoji, 123, globe and shift darken while held, the way the native ones do — letters do not, because there the key-pop is the feedback. The held tint is measured rather than guessed at, in both appearances.

The space bar is bigger than it looks

spaceBarTopExtensionMeasured

On the native keyboard, aiming at space and getting a "c" never happens — the bar quietly claims a margin of the row above it. It is specific to the space bar, not a general downward nudge: the boundary between two letter rows measured as exactly where it is drawn. The engine reproduces both, and the margin is a parameter you can take back to zero.

Callouts anchor the pressed character above the key

Measured

The plain, un-accented character sits directly above the key you are holding and starts selected, so your finger never travels to pick it. Keys with many variants (e, u, o) overflow the bar to both sides, as native does, which keeps the outer accents reachable instead of jammed against the edge of the screen.

Sound and haptics

keySound / hapticFeedback

The engine plays the system key clicks directly, so key sound needs no host setup and works without Full Access, and it respects the silent switch. Haptics do need Full Access, which is an iOS rule rather than ours.

The native look is versioned, and pinnable

nativeDesignID

Parity is measured against one iOS release, and Apple redraws the keyboard between releases. So each round of measurement is stored as a named snapshot, and the engine renders the newest one no newer than the device it is running on. When a future iOS changes the drawing, a new snapshot lands beside the old one rather than replacing it — your users' keyboard does not change shape underneath them, and you can pin a look deliberately if you need to.

Rendering and theming

One SwiftUI view, three slots, and a style protocol. Your app supplies the theme; the kit imports none of your code.

One view with three slots

PressboardKeyboardView

The keyboard is a SwiftUI view with a toolbar slot above the keys, a buttonContent slot that overrides the content of any key, and an emojiKeyboard slot. Widths are solved by KeyboardWidthSolver, which reproduces native alignment: letters share one width and the flexible keys divide what is left.

Styling is a protocol, not a fork

KeyboardStyleProvider

NativeKeyboardStyle is the default and carries the measured native look. A host implements the protocol only for what it wants to change and inherits the rest, so restyling one key colour does not mean re-deriving the whole keyboard.

Six built-in themes

theme

Native, Midnight, Ocean, Sunset, Forest and Graphite. A theme is a single ThemePalette (light and dark) that recolours the whole keyboard — keys, text, accent, callouts, the key-pop and the suggestion bar all read their colour from the style, so nothing is left behind on the old palette. Your own theme is a ThemePalette plus ThemedKeyboardStyle.

It sits on the system's glass, not on a grey rectangle

translucentBackground

The modern iOS keyboard is not an opaque block: it floats on a translucent backdrop and blends with the app behind it. The engine draws itself so that backdrop shows through, with keys translucent on the glass, which is the difference between a keyboard that belongs to the system and one that was pasted over the screen. Non-native themes are solid by design and switch the glass off.

Uniform key colour

uniformKeyColor

Gives shift, backspace, return, globe, mic and 123 the letter-key colour instead of the darker native control colour. Off by default, because off is native; on in the kit's own recommended preset.

Space-bar label

showSpaceLabel

Turn the word "space" off to leave the bar blank — useful when you would rather show only a language badge in the corner, as native does with several keyboards enabled.

Layouts

Layouts are data. Adding a language is a table and an enum case; the engine does not learn about it.

Data-driven layout tables

StandardLayoutResolver

A locale's table holds the letter rows, the 123 and symbol pages, and the per-locale long-press callouts. The resolver assembles the full layout from it — shift and backspace row, bottom row, and the application of shift to both letters and callouts.

Five key arrangements

layoutTypeOverride

QWERTY, AZERTY, QWERTZ, Dvorak and Colemak, with authentic letter rows rather than a permuted QWERTY. Leave it nil and each locale uses its own default: English QWERTY, Czech and German QWERTZ, French AZERTY.

The bottom row lines up with the letter grid

Measured

Every key in the bottom row shares an edge with a letter above it, and the same holds on the numeric and symbol pages — which is why the native keyboard reads as a grid rather than as five rows that happen to be the same width. Regression tests hold the alignment in place, because this is exactly the kind of thing that drifts a point at a time and is never noticed until it looks wrong.

A domain key on URL fields

adaptToInputType

A URL or web-search field gets a .com key whose callouts lead with the country TLD of every language you have enabled — a Czech keyboard offers .cz, a German one .de, both enabled offers both — followed by the generic list. Languages that map to no single country add none. The key never inserts on key-down even when key-down insertion is on: a multi-character key is not pressed by accident, and inserting on press made holding it useless.

The keyboard follows the field

adaptToInputType

Email fields get "@" and ".", number fields get a digit pad, and the return key becomes Go, Search, Send and so on. Turn it off and you get the plain letters layout and a "return" key in every field.

Typing

The part users feel but cannot describe: when a character commits, which key a borderline touch belongs to, and what happens to the space you typed a moment ago.

Insert on key-down

insertOnKeyDown

Characters and the space bar commit the instant the key goes down rather than on release, so typing reads as immediate. A long-press for accents replaces what was inserted, a glide undoes it, dragging off the key cancels it. Off by default because native commits on release.

Space commits on release

spaceCommitsOnRelease

Press and hold the native space bar and nothing is typed until you lift — that is how it can still tell a tap from a cursor drag. The engine does the same, and a held space is flushed the moment another key commits, so fast typing keeps its order.

A single multi-touch input layer

multiTouchInputLayer

Fast typing with two thumbs is where naïve keyboards fall apart: overlapping presses get reordered, buffered or dropped outright. The engine runs every touch through one state machine that resolves them into the order they were actually made — and that same machine owns key-down commit, the accelerating backspace, long-press callouts, the spacebar cursor drag and slide-to-type, so no two gestures ever fight over a finger. On in .pressboardDefault, off in .standard.

Predictive key targeting

predictiveKeyTargetingMeasured

A touch on the line between two neighbouring letters leans toward the one that keeps the word you are typing a real word — which is a thing the native keyboard does, and can be demonstrated on a device. It only ever settles a tie: a touch comfortably inside a key is never reassigned, and with no word in progress or no dictionary for the language it does nothing at all.

Shift, auto-shift and caps lock

enableCapsLock / autoShiftToLowercase

A tap toggles shift, a quick double-tap engages caps lock, and a single shift falls back to lowercase after one character. The shift key shows its state with the filled glyph and the light fill, as native does. Turn auto-shift off for a sticky shift.

Auto-capitalisation

autoCapitalization

A capital at the start of the field, after a full stop, exclamation mark or question mark followed by a space, and after a newline.

The double-space "." shortcut, with its window

periodShortcut, periodShortcutWindowMeasured

Two spaces after a word become a full stop and a space. What most reimplementations miss is that native gives you a window: the second space has to arrive promptly, so a space pasted in, or typed a minute ago, never expands into a sentence the user did not write. The engine has the window too, measured rather than invented, and exposed as a parameter if your app wants a different one.

Punctuation spacing

punctuationSpacing

Typing one of . , ! ? ; : after a stray space moves the space to the other side of the mark, turning " ." into ". " the way native does.

Typed emoticons survive it

emoticonAwarePunctuation

: and ; open smileys, so the fixup above would mangle "ahoj :)" into "ahoj: )". The mouth cannot be predicted at the moment the colon is typed, so the move is undone as soon as a mouth character follows immediately. A space between mark and mouth closes the window; switching page or pressing shift does not, so "hi :D" survives.

A correction you rejected stays rejected

suppressPunctuationSpacingAfterEdit

If the user backspaces a spacing fixup all the way back to the word it moved a space past and retypes it their way, the fixup does not fire there again — whatever mark they type. It survives retyping the space itself, and is cleared once the cursor moves elsewhere.

Back to letters after a space

returnToLettersAfterSpaceMeasured

A space or a newline on the 123 or symbol page flips back to letters — and so does one further key, which nobody would guess and which we found by working through the native page one key at a time. It makes sense once you see it, and it is the kind of detail that separates a keyboard people keep from one they switch off.

Smart punctuation

smartPunctuation

Straight quotes become locale-aware curly ones — English opens and closes high, Czech opens low — the apostrophe is typographic, and a double hyphen becomes an em dash.

Delete by word

deleteByWord

Holding backspace accelerates and then starts deleting whole words, at the native cadence.

One-handed mode

oneHandedMode, oneHandedScale

The keys shrink and move to one side, with a chevron in the gap to return to full width. How far it shrinks is a parameter rather than a constant. The whole bottom row, globe and mic included, moves with the keyboard as one block, as native does.

A one-handed picker on the globe key

oneHandedGlobeMenu

Holding globe opens a menu with the system keyboard switcher on top and a left / off / right one-handed picker below it. The system's own menu cannot be extended with custom rows, so the engine draws its own.

Key-press animations can be switched off

keyAnimationsEnabled

Native animates the press feedback, so animating is the default. Turning it off applies those transitions instantly — an experiment for checking whether already-instant key-down insertion only reads as slow because of what is animating on top of it.

Autocorrect and prediction

Correction that is not quietly English-only, and a suggestion bar that has something to say after the word is finished.

Autocorrect on space

autoCorrection

The word being typed is replaced with the correction when you commit it, drawing on the same system spell checker the native keyboard uses.

Correction that works outside English

LayeredSpellChecker

The system spell checker is not equally good in every language — in some it will tell you cheerfully that every word you type is spelled correctly, which is how a keyboard ends up silently correcting nothing at all. The engine does not take that at face value. It layers the system's judgement with its own, falls back where the system has no data, and restores the accents nobody bothers to type in the same pass, so "delas" becomes "děláš". A word that is valid in any language you have enabled is never rewritten.

Never corrects a lone character or a token with a digit

autoCorrectMinimumWordLength

Native never corrects a single letter — "Add 100 g of flour" has to survive — and neither does this, nor H2O or a flight number. The handful of deliberate per-language rules are exempt: a lone "i" is capitalised in English, and left alone in Czech, where it is a word in its own right.

A correction you undid is not reapplied

suppressAutocorrectAfterEdit

Backspace into a corrected word to change it and autocorrect leaves it alone until you move on — space, return, or the cursor going elsewhere. Native reads that edit as "I rejected the correction", and so does this.

Next-word prediction with learned pairs

nextWordPrediction

Once a word is finished the bar offers continuations instead of going blank. Two sources: pairs learned from what the user actually types, ranked first, and seed bigrams the host can supply. Learning is what makes it useful with no data shipped at all. It is bounded, so it cannot grow into the extension's memory limit, and the host persists it so it survives the process being killed. With nothing learned and no seed data it offers nothing, because guessing would be worse.

Suggestions in an empty field

emptyFieldSuggestions

A field nobody has typed into yet gets three opening words rather than an empty bar, as native does. Which three is the provider's call, and the host can pass its own.

The three-slot bar

predictiveText

SuggestionBar is the native three-slot layout — what you typed in quotes, the highlighted primary completion, and an alternative — dropped into the toolbar slot. Over a number, phone or decimal pad there is no bar at all, because native has none there and drawing one would make the keyboard taller for nothing.

Word lists are in the box

DictionarySuggestionProviderOptional module

The autocomplete module bundles 39 word-frequency lists, loaded lazily per language, so suggestions and swipe typing work before you have supplied anything. They are built from a conversational corpus rather than a news one, because people type to each other, not to a newsroom. Pass your own list to override, or no provider at all for no suggestions.

The lists are cleaned, not just imported

A raw frequency list carries the misspellings of the corpus it came from — in Czech, for instance, a shadow copy of half the language with its accents stripped off. Those were cleaned out where they are not themselves real words, which is what lets "cas" correct to "čas" while genuine words that merely look like accent-free spellings survive untouched.

Arithmetic results

showMathResults

Type 12*3 in a field and the bar offers the expression, the expression with its result, and the result on its own; picking any of them replaces the expression.

Emoji suggestions

emojiSuggestions

The word being typed is mapped to an emoji and offered as the highlighted slot — bilingually (EN and CS), ignoring case and diacritics.

Gestures and emoji

Glide, callouts, the cursor drag on the space bar, and the emoji panel — the parts where a keyboard is judged by feel rather than by feature list.

Slide to type

slideToType

Drag across the keys to write a word; the decoder resolves the path against the host's dictionary. A glide beats a long-press callout: a letter key yields to the swipe once the finger is travelling, while holding still keeps the accents. Candidates are ranked by word frequency, with length only as a weak correction — ranking by length used to hand the win to longer, rarer words. Multiple enabled languages are compared fairly, by rank within their own list rather than by position in a merged array.

Candidates appear during the glide

slideChanged

The bar updates while the finger is still moving; on lift the best word is inserted and the alternatives stay in the bar, so a tap swaps the inserted word the same way a completion does. Mid-glide candidates are decoded first-pass, so a half-finished path suggests less well than a finished one.

Diacritics on a swipe — and a deliberate divergence

slideToTypeRestoresDiacriticsMeasured

A glide can only ever cross the base letters, so an accented word has to be inferred. Measured on a device, native inserts the un-accented form. The engine defaults to inserting the correct one — "máš" rather than "mas" — because it already knows which word you traced. This is one of the few places we knowingly differ from native, and it is one flag back to exact parity.

Long-press callouts

characterPreview

Holding a key opens the bar of secondary characters — complete diacritics for every letter, native iOS plus Czech — selected by dragging and inserted on release. The bar is clamped to stay on screen at the edge keys.

Spacebar cursor drag, in two dimensions

Measured

Dragging along the space bar moves the caret without typing a space. It moves at the measured native rate and then accelerates for a long run, so it stays precise for a one-character correction and quick across a sentence. All keys blank during the drag and neither predictions nor letter case update, exactly as native does.

Vertically it moves by sentence, not by line

A keyboard extension cannot see where a line of text visually breaks — it has no access to the field's width, font or wrapping, and sees only a window of text around the caret. So an up or down drag jumps by sentence instead of pretending to know about lines. Nothing is guessed, and the threshold is set high enough that an ordinary wobble during a sideways drag does not trigger it.

Caret moves are coalesced so you can see them

cursorDragUpdateInterval

Every caret move is a call across a process boundary, and a drag on a high-refresh display produces far more of them per second than the app you are typing into can repaint. Push them all and the caret simply vanishes mid-drag. The engine batches them instead, which is why the caret you are dragging stays on screen.

A haptic tick when the drag engages

cursorDragEngageHaptic

Native gives this one even when keyboard haptics are off, because it signals a mode change rather than a keystroke, and so does the engine. In an extension any haptic still requires Full Access.

Emoji panel

emojiKeyOptional module

Categories, recents, a four-row grid, and a skin-tone bubble on long press with slide-to-select. Recents come from an injected store, and a host that has its own panel can drop it into the slot instead.

Full-text emoji search

emojiSearchOptional module

Hand-curated keywords in English and Czech, insensitive to case and accents, with a search mode where typing goes into the query and results update live.

The emoji category bar matches native

Measured

The "ABC" key, the active category marked the way native marks it, and icon sizes and shades measured off the system panel rather than approximated. One honest gap: a few of the native category icons are Apple's own artwork, which is not available to anyone else, so those are the nearest system equivalents.

Dictation key

dictation

The mic key and its action and hook are in the engine; the speech-to-text is yours to supply, because microphone and speech access belong to the host app.

Languages

73 locales ship with the kit — layout table, long-press callouts, space-bar badge and, for most of them, a word-frequency list that drives predictions and swipe decoding. Adding one is a data table plus a case in KeyboardLocale; the engine is not touched.

LanguageSpace-bar badgeDomain key offers
EnglishENgeneric list only
ČeštinaCZ.cz
DeutschDE.de
FrançaisFR.fr
EspañolES.es
ItalianoIT.it
PolskiPL.pl
Português (Brasil)BR.br
TürkçeTR.tr
РусскийRU.ru
ΕλληνικάEL.gr
العربيةARgeneric list only
עבריתHE.il

…and 60 more, including Ukrainian, Hungarian, Vietnamese, Persian, Georgian, Armenian, Northern and Inari Sámi, and the regional variants (en-GB, fr-CA, de-CH, es-MX).

Which languages predict

53 of the 73 resolve to one of the 39 bundled frequency lists — regional variants share their base language's. The remaining 20 — among them Belarusian, Mongolian, Welsh, Irish, Hawaiian and the Kurdish variants — type normally but come without predictions or swipe decoding, because no source list exists for them yet. A test keeps the flag and the bundled data from drifting apart, and passing your own list for any locale is one initialiser argument.

Scripts beyond Latin

Cyrillic (Russian, Ukrainian, Bulgarian, Macedonian, Serbian, Belarusian, Mongolian, Kazakh, Chuvash), Greek, and the right-to-left scripts — Arabic, Hebrew, Persian, Kurdish Sorani — as well as Armenian and Georgian.

Regional variants are their own locales

en-GB, en-US, en-AU, en-CA, fr-CA, fr-BE, fr-CH, de-AT, de-CH, nl-BE, pt-BR, es-419, es-MX and Norwegian Nynorsk each exist separately, so a Swiss German keyboard is a Swiss German keyboard rather than a German one with a note attached.

Configuration

The rule the kit is built on: every behaviour of the keyboard is a property, its default is the native behaviour, and a customer can switch any of it off without touching the engine.

There are 51 configurable properties on KeyboardBehavior. Text-level logic — auto-capitals, punctuation, deletion — is exposed as pure functions that take the flag and return nothing to do when it is off, so a disabled behaviour is genuinely absent rather than suppressed downstream. View-level behaviour is read straight off the same value by the render view. A feature without a property is treated as a defect here, because the customer cannot turn it off.

// Start from the native keyboard, then opt out of what you don't want.var behavior = KeyboardBehavior.standard behavior.slideToType = false            // no swipe typing in this appbehavior.periodShortcutWindow = 0.5     // stricter double-space windowbehavior.characterPreviewStyle = .floatingbehavior.theme = .midnight PressboardConfiguration(    behavior: behavior,                 // omit it and you get .pressboardDefault    locale: .czech,    words: [.czech: czechWords, .english: englishWords])

Two presets

.standard is pure native. .pressboardDefault is the kit's recommendation and what you get if you do not pass one: native plus a uniform key colour, no globe or mic key, key-down insertion, the multi-touch input layer and predictive key targeting.

Adding a flag does not reset your users' settings

Codable

The value is Codable with every key decoded optionally, falling back to the same default the initialiser uses. With synthesised decoding, one new property makes every previously stored settings blob fail to decode — which, for a host persisting this in an App Group shared with its extension, means silently wiping every choice its users made on each SDK update that adds a flag.

Everything host-specific is injected

Theme, toolbar UI, suggestion source, word lists, feedback and settings storage all come in from outside. The kit imports no host code, has no server and no runtime call-home.

Performance and memory

A keyboard extension gets a fraction of an app's memory, and it is killed rather than warned when it overruns — the keyboard just vanishes mid-sentence. The kit is shaped around that.

Optional modules are separate products

The core engine and the layout tables are the minimum. Autocomplete and emoji are separate Swift package products precisely so an app that does not use them never links them, and the engine's footprint is measured inside a real extension rather than estimated from the code.

Typing does not re-render the keys

KeyGridMeasured

Drawing three dozen keys is not free, so the engine keeps that work off the keystroke path entirely: typing a letter mid-word redraws the suggestion bar and nothing else, verified by counting renders on a device rather than by reasoning about it. The integration guide covers the other half — how to keep your own toolbar's state from dragging the keys back into every keystroke.

Suggestions are looked up, not searched

The bundled provider indexes its word list, so the call that runs on every single keystroke stays far below the cost of a frame even against a full-size dictionary.

Insert latency

Measured

Measured from lifting a finger to the character appearing, the engine is at the floor of what the platform's own gesture path allows.

Memory diagnostics you can switch on

memoryDiagnosticsInterval

The keyboard can log its own footprint every N keystrokes while you investigate. Off by default; it costs one system call per interval and nothing else.

Accessibility

A keyboard that VoiceOver cannot press is not a keyboard.

Every key is one element, named and activatable

Each key is a single accessibility element carrying its character in the current case — or space, delete, shift, return, letters, next keyboard, dictate — with the keyboard-key trait and its own activation action. Assistive activation delivers no touch at all, so without that action a key could be focused but never pressed. A host-supplied overlay, such as a language badge on the space bar, is hidden from accessibility so it cannot turn the key into a container and take its name away.

The suggestion bar is readable and tappable

Each suggestion is its own element named by the word it will insert, and the highlighted correction announces itself as a suggestion. Verified in a real extension.

Testing

A demo app is not a keyboard extension, and treating the two as equivalent has cost us whole days. The test setup is built around that distinction.

466 tests, run where the code actually runs

The engine's logic — hit-test geometry, the touch state machine, callouts, the cursor drag, autocorrect, prediction, layout solving — is covered by unit tests run on the simulator rather than on the host Mac, because building for macOS silently compiles the iOS-only half of the suite away.

Touch behaviour is proven inside a real extension

XCUITest

A keyboard extension is hosted out of process, and behaviour that works in an in-app preview can fail there completely — we have lost days to exactly that. So the UI tests tap exact coordinates inside the really rendered key rectangles, read back from the running keyboard rather than from a second copy of the engine's own maths, and the important ones run against the real extension while typing into another app.

Some thresholds are measured against the native keyboard

Measured

Where iOS exposes no value to read, we measure it: a test class drives the real native keyboard and our own extension through the same harness in the same app, so the two are directly comparable and our constants are answers rather than plausible guesses. It is slow and exploratory, and is re-run when those constants are retuned.

The harness works in your app too

The key identifiers are public API and the geometry helper is plain XCTest, so you can point a UI test target at whatever app you type into and assert on real key positions, with nothing PressboardKit-specific required.

Read the integration guide

A working keyboard is a subclass and a configuration value. The guide covers action routing, styling, App Group settings, the autocomplete and emoji modules, and the performance notes referenced above.