Skip to content
PressboardKit
Layouts and languagesBrowse docs

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, .phonePad or .decimalPad fields; 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.suppressesSuggestionBar exposes 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.

Edit this page on GitHub