Guides
Layouts and languages
How a layout is resolved from context, the five key arrangements, adapting to the edited field, and what adding a language actually involves.
A layout is not configuration — it is derived. KeyboardContext holds the mutable state,
StandardLayoutResolver turns that state into a KeyboardLayoutDefinition, and the view draws
the definition. Change the state, re-resolve, publish the new layout: that is what re-renders the
keys.
The context
public final class KeyboardContext {
public var locale: KeyboardLocale // .english / .czech / …
public var layoutTypeOverride: KeyboardLayoutType? // nil = the locale's default
public var keyboardType: KeyboardType // .alphabetic / .numeric / .symbolic / .emojis
public var keyboardCase: KeyboardCase // .lowercased / .uppercased / .auto / .capsLocked
public var inputType: KeyboardInputType // .normal / .email / .url / .numberPad / …
public var returnKeyType: ReturnKeyType // .default / .go / .search / .send / …
public var enabledLocales: [KeyboardLocale]
}
effectiveLayoutType resolves layoutTypeOverride against locale.defaultLayoutType.
Resolving
There is a convenience overload that reads the whole context, and an explicit, context-free one:
// Convenience — pass the key-visibility toggles from your behaviour.
layout = StandardLayoutResolver.layout(
for: context,
adaptToInputType: behavior.adaptToInputType,
dictation: behavior.dictation,
emojiKey: behavior.emojiKey,
nextKeyboardKey: behavior.nextKeyboardKey)
// Explicit — pure, no context object.
layout = StandardLayoutResolver.layout(
locale: .english,
layoutType: .qwerty,
keyboardType: .alphabetic,
keyboardCase: .lowercased,
inputType: .normal,
returnKeyType: .default,
dictation: false,
emojiKey: true,
nextKeyboardKey: true,
locales: [.english, .czech])
Re-resolve whenever the case, the page, the locale or the arrangement changes.
PressboardController does this for you and republishes layout.
Key arrangements
KeyboardLayoutType is .qwerty, .qwertz, .azerty, .dvorak or .colemak, with the real
letter rows for each. layoutTypeOverride forces one; nil, the default, uses the locale’s own —
English resolves to QWERTY, Czech to QWERTZ.
behavior.layoutTypeOverride = .dvorak
The resolver wraps the rows generically and derives the long-press callouts from the characters, so an arrangement works without any per-arrangement special-casing.
Pages
KeyboardType is .alphabetic, .numeric, .symbolic or .emojis. On every page the bottom row
lines up with the letter grid above it the way native does, which is what makes the keyboard read
as a grid rather than as rows that happen to be the same width. The resolver handles that for you —
including as keys appear and disappear with dictation and nextKeyboardKey — and regression
tests hold it in place.
returnToLettersAfterSpace flips back to the letters page after a space, a newline or an
apostrophe typed on the 123 or #+= page. The apostrophe is in that set because it is the one
punctuation mark that triggers the return on the native keyboard — which makes sense, since a word
continues after it.
Adapting to the field
With adaptToInputType on, the keyboard follows the edited field’s traits: an email field gets
@ and ., a URL field gets a .com key, a number field gets a digit pad, and the return key
becomes Go, Search, Send and the rest. The engine ships the UIKit mappings:
context.inputType = KeyboardInputType(textDocumentProxy.keyboardType ?? .default)
context.returnKeyType = ReturnKeyType(textDocumentProxy.returnKeyType ?? .default)
Then re-resolve. PressboardInputViewController does this from textDidChange, which is a
callback with a live input session by construction.
Two notes worth knowing:
- iOS never presents a third-party keyboard for
.numberPad,.phonePador.decimalPadfields; it shows its own pad. Those layouts therefore only apply when you embed the engine as an in-app keyboard. - Over a number, phone or decimal pad the suggestion bar is suppressed, because native shows none.
PressboardController.suppressesSuggestionBarexposes that decision.
The domain key
A URL or web-search field gets a .com key next to the space bar. Its long-press callouts are
built from every language you have enabled, in that order, followed by a generic list:
controller.setLocales([.czech, .german]) // → .cz, .de, then .net, .org, .eu, …
That set reaches the layout through KeyboardContext.enabledLocales, which the controller keeps
in sync; a host driving the resolver itself passes locales: instead. A language with no single
country — English, Arabic, the Sámi languages — contributes nothing, because guessing a country
from a language would be worse than offering nothing. The bar is capped at DomainKey.maxCallouts
so it stays reachable with one thumb.
The domain key never commits on key-down, even with insertOnKeyDown on: a multi-character key is
not typed fast or by accident, and committing it early made its own long-press unusable — .com
was typed before the callout bar opened, so picking .eu produced .com.eu.
Languages
KeyboardLocale carries 73 cases, covering Latin, Cyrillic, Greek, Arabic, Hebrew, Armenian and
Georgian scripts, plus the regional variants that differ in layout or in the return key’s label.
Each case knows its identifier, its displayName, its defaultLayoutType and its
topLevelDomain.
Several languages can be enabled at once. The first is primary: it drives the key layout, the callouts, smart punctuation and spell checking. All of them are searched for suggestions and for slide-to-type, with their dictionaries merged, so the keyboard can complete words from more than one language at a time.
PressboardConfiguration(locales: [.czech, .english])
// or, at runtime:
controller.setLocales([.czech, .english])
The space bar shows an indicator of the enabled languages in its corner, as native does.
Adding one
Adding a language is a data-table change inside PressboardKitLayouts, not an engine change: the
letter rows live in CharacterRows, the long-press callouts and the punctuation pages in tables
beside it, and the locale itself is a case on KeyboardLocale. Nothing in the resolver or the
render view needs to know about it.
That also means a new language is a change to the package rather than something a host injects. If
you only need a different word list for an existing language — a domain-specific dictionary,
or a larger one — that is injection, and it goes through PressboardConfiguration:
PressboardConfiguration(
locales: [.english],
words: [.english: myEnglishWords] // most-frequent first
)
Leave words empty and the bundled frequency lists are used.
Next: Autocomplete.