Skip to content

Instantly share code, notes, and snippets.

@jokull
Created July 19, 2026 11:23
Show Gist options
  • Select an option

  • Save jokull/2441df4c982ec689d7428e57dff73ba4 to your computer and use it in GitHub Desktop.

Select an option

Save jokull/2441df4c982ec689d7428e57dff73ba4 to your computer and use it in GitHub Desktop.
The iOS keyboard punctuation & whitespace spec — the undocumented behavior every third-party keyboard reimplements

The iOS keyboard punctuation & whitespace spec

The undocumented behavior that every third-party iOS keyboard has to reimplement — reverse-engineered, with the escape hatches spelled out.

Compiled while building Lyklaborð, a privacy-first Icelandic keyboard. Corrections welcome.


Why this document exists

There is no vendor spec for how the iOS keyboard handles spaces, periods, commas, and sentence boundaries. Apple's Custom Keyboard guide hands a keyboard extension exactly three tools — insertText(_:), deleteBackward(), adjustTextPosition(byCharacterOffset:) — and nothing else. A custom keyboard inherits none of the system keyboard's punctuation or whitespace smarts. Double-space→period, space collapsing, smart quotes, autocapitalization: the system keyboard implements every one of them in its own (closed) code, and SwiftKey / Gboard each reimplement them independently — and diverge.

So the "spec" below is assembled from three sources:

  1. The behavior rules as they exist in a mature open-source keyboard engine (KeyboardKit's StandardKeyboardBehavior + its UITextDocumentProxy sentence/word extensions), which mirror what the system does.
  2. Empirical capture — an accessibility-driven replay harness (XCUITest taps the real on-screen keys and records the text buffer after every tap), run against the real keyboard in the simulator.
  3. Observation of native iOS + SwiftKey on the same inputs.

The knobs Apple does document are a handful of user settings ("." Shortcut, Smart Punctuation, Auto-Correction, Auto-Capitalization) and a few UITextInputTraits (smartQuotesType, smartDashesType, smartInsertDeleteType, autocapitalizationType, autocorrectionType). Those are the switches, not the algorithms, and even Apple's docs call the smart-* traits "limitedly documented."


1. Double-space → ". " (the "." Shortcut)

The signature behavior. Typing two spaces in a row after a word replaces them with a period and one space.

Fires on the release of the second space, only when ALL of these hold:

  • the two space taps happened within ~3 seconds of each other (endSentenceThreshold);
  • the cursor is at a new word (i.e. the preceding space put you at a word boundary);
  • the cursor is not already at a new sentence (the text doesn't already end in . / ! / ? );
  • the buffer ends with two spaces at that moment.

What it does: delete all trailing spaces, then insert ". ". So word␣␣word. . Note the collapse is a side effect of the delete-then-insert — see §2.

on space.release:
    if  withinThreshold(lastSpaceTap, now, 3.0)
    and cursorAtNewWord
    and not cursorAtNewSentence
    and bufferEndsWith("  "):
        while bufferEndsWith(" "): deleteBackward()
        insertText(". ")

Escape hatches — how a deliberate double space survives

Because the rule is conjunctive, a double space is left intact (you keep ␣␣) whenever any condition fails. In practice that means:

  • You pause between the two spaces (> ~3 s) → not treated as a sentence-ender.
  • You're already at a sentence start — the text ends in . already, so a further double space stays .␣␣ rather than becoming .␣. (the rule refuses to stack periods).
  • The cursor isn't right after a word (start of field, after other punctuation).
  • You're inside a URL/email/number field (see §7) — the whole feature stands down.

This is the entire "the user actually wants two spaces" story: there is no separate space-collapse pass that would eat a deliberate double space. If double-space→period doesn't fire, your two spaces remain two spaces (see §2). Monospace/code alignment therefore survives as long as you're not triggering the sentence-ender.

Settings gate

Governed by Settings → General → Keyboard → "." Shortcut. Crucial gotcha for keyboard developers: a third-party keyboard cannot read that system toggle — there's no API — so it must ship its own setting and its own default. (SwiftKey does exactly this.)


2. Space collapsing

There is no general "collapse multiple spaces into one" behavior in the native baseline. The only place spaces collapse is inside the sentence-ender in §1 (the while bufferEndsWith(" "): deleteBackward() step), which runs only when §1 fires.

Consequence: word␣␣␣ (three spaces, typed fast, after a word) becomes word. ␣ — the first two collapse into . and the third is a normal trailing space. But word␣␣ with a pause, or ␣␣ with no preceding word, stays exactly as typed.

If your keyboard wants to be more helpful than Apple (collapse a stray ␣␣ even when the sentence-ender declines), that is a deliberate divergence, and you must still exempt the deliberate-double-space cases above — otherwise you break code and ASCII layout.


3. Auto-capitalization

The first letter of a new sentence is capitalized: at the start of the field, and after a sentence-terminator followed by a space (. , ! , ? ).

Escape hatches: mid-word; after non-terminal punctuation (, ; :); in a field whose autocapitalizationType is .none / .words / .allCharacters; inside URL/email/password fields. Governed by Auto-Capitalization in Settings and the field's autocapitalizationType trait.


4. Smart punctuation

Text substitutions performed as you type. Locale-aware.

Input Output Notes
" (straight) "" (curly), contextual open/close Locale-specific: en "…", Icelandic/German „…", French «…»
' (straight) '' apostrophe vs closing single quote by context
-- (em dash)
... (ellipsis)

Open vs close is decided by the preceding character (whitespace / opening bracket → opening quote; otherwise closing).

Escape hatches: disabled in password, URL, and email fields; controllable per-field via smartQuotesType / smartDashesType. Gotcha: smart punctuation can still be applied when text is inserted via UITextView.replace(_:withText:) even with the trait set to .no — so it's not a hard off switch for programmatic insertion.

Governed by Settings → General → Keyboard → Smart Punctuation.


5. Deferred autocorrect on terminal punctuation

When you end a word with . , ! ?, the pending autocorrection is applied to the word first, then the punctuation is appended: teh.The.. The punctuation acts as a word-committing delimiter, exactly like a space, but keeps the delimiter you typed.

Escape hatch: the exact word you typed must remain available (usually surfaced verbatim in the suggestion bar) so autocorrect near-misses on names/URLs/slang are recoverable. And a period that is part of a token (.com, 3.14, v2.0) must not trigger a correction — see §7.


6. Whitespace around autocomplete acceptance

Accepting a suggestion (tap, or space-to-commit) inserts the word plus a trailing space. Then:

  • Typing terminal punctuation removes that auto-space and attaches the punctuation: word␣ + .word. (not word .).
  • Typing another word just continues after the space.
  • Backspacing once removes the auto-space (smart delete), not just the last character, in some field configurations (smartInsertDeleteType).

Escape hatch: if the next character is not a delimiter (e.g. it forms .net, .com), the space-removal/attachment must be discarded so the token survives.


7. The escape-hatch matrix (where all smarts stand down)

The single biggest source of "this keyboard is fighting me" bugs is applying the above in contexts where they're wrong. The baseline suppresses all of §1–§6 in:

  • URLs / emails / domainskeyboardType == .URL / .emailAddress, or heuristically when the token contains @, ://, or a word.word shape. No autocap, no smart quotes, no double-space-period, no autocorrect.
  • Numbers — decimal separators, thousands separators, ordinals, times, versions, phone numbers. A . or , adjacent to digits is arithmetic/formatting, never a sentence end. (Locale matters — see §8.)
  • Secure fieldsisSecureTextEntry == true: no autocorrect, no smart punctuation, no learning.
  • Code / monospace / deliberate whitespace — nothing should collapse or reflow the user's spacing.
  • Quoted foreign terms — a word inside quotes is often a deliberate foreign spelling; be reluctant to autocorrect it.

A keyboard that respects a valid typed word (never silently replaces something already spelled correctly) gets most of this right by construction.


8. Locale specifics (the part generic keyboards get wrong)

Using Icelandic as the worked example, but every locale has its own:

  • Quotes: Icelandic opens with (U+201E) and closes with " (U+201C) — not "…". German is the same; French uses «…» with (thin) spaces inside.
  • Decimal comma: 3,14 — a comma between digits is a decimal point, not a clause break.
  • Thousands dot: 1.000 — a period between digits is a grouping separator, not a sentence end.
  • Ordinals: 1., 21. — a period after a number is an ordinal marker (fyrsti, tuttugasti og fyrsti); it must not trigger the sentence-ender or auto-cap the next word.

If your smart-punctuation and number handling are locale-blind, you will corrupt prices, dates, and ordinals in exactly the languages that most need a good keyboard.


How to observe / verify this yourself

Because none of it is documented, the only ground truth is measurement. A repeatable method:

  1. A minimal host app with a single UITextField (known input traits, host autocorrection off so only the keyboard's behavior is in play) and a mirror label exposing the text to the accessibility tree.
  2. An XCUITest that taps the real on-screen keys by accessibility label (app.keys["space"].tap()) through a scripted catalog of tap sequences, and records the buffer after every tap — so you capture the step-by-step evolution, not just the end state.
  3. Run the same catalog against native, SwiftKey, and your keyboard; diff the columns.

Caveats learned the hard way: enabling a third-party keyboard + Full Access in the simulator is a one-time manual step (XCUITest can't flip Settings toggles); the on-screen keyboard only appears with "Connect Hardware Keyboard" off; and synthetic tap cadence can outrun a keyboard's async commit queue, so keep inter-key delays human (~180–250 ms) or you'll measure races instead of behavior.


The one-paragraph summary

iOS gives custom keyboards no punctuation/whitespace behavior for free; the system keyboard's smarts are undocumented and each third-party keyboard reimplements them. The core rules — double-space→". " (conjunctive conditions, ~3 s window, collapses trailing spaces, refuses to stack periods), sentence auto-capitalization, locale-aware smart quotes/dashes/ellipsis, deferred autocorrect on terminal punctuation, and auto-space management around suggestions — are simple individually but interact, and are only correct when suppressed wholesale in URLs, numbers, secure fields, code, and quoted foreign terms. Deliberate double spaces survive precisely because there is no separate collapse pass — the two spaces are only ever touched by the sentence-ender, and only when its conditions all hold.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment