What an iOS keyboard can't do without Full Access
Every third-party keyboard on iOS runs in one of two sandboxes. Which one is decided by a single key in the extension's Info.plist, RequestsOpenAccess, and by a switch the user may or may not turn on in Settings. I built a snippet keyboard, Light Clipboard, entirely inside the stricter of the two. This is what that sandbox allows, what it refuses, and what it did to the design.
Two sandboxes
With RequestsOpenAccess set to false, Settings never shows an "Allow Full Access" switch for the keyboard. Apple's documentation lists what such a keyboard gets:
- everything a basic keyboard needs, plus the system lexicon and the user's text shortcuts
- no network
- no file system beyond its own container, except read-only access to the containing app's shared containers
- no microphone or speaker
- no iCloud, Game Center or In-App Purchase
The same page states the consequence in one line: without open access, "keystrokes can't be sent back to your server or anywhere else."
Set the key to true and the user gets the switch, along with the system warning about everything they type. If they turn it on, the keyboard gains the network and a writable shared container.
Most keyboards that ask do so because they need something from that second list: sync, cloud suggestions, GIF search. A snippet keyboard needs none of it. It has to read a list and insert a string.
The app writes, the keyboard reads
A read-only shared container settles the architecture. The app is the only writer:
// App
let defaults = UserDefaults(suiteName: "group.com.example.snippets")
defaults?.set(try? JSONEncoder().encode(items), forKey: "items")
The keyboard reads the same key every time it appears, because iOS keeps the extension process alive between appearances and the list may have changed in the meantime. To avoid rebuilding the grid each time a text field gets focus, it compares the raw bytes first:
// Keyboard extension
private func loadItems() {
let data = userDefaults?.data(forKey: storageKey)
guard data != lastLoadedData || lastLoadedData == nil else { return }
lastLoadedData = data
if let data, let decoded = try? JSONDecoder().decode([Snippet].self, from: data) {
items = decoded
} else {
items = []
}
collectionView.reloadData()
}
Nothing flows the other way. There is no path by which the keyboard could report anything, to the app or to anyone else.
Insert, don't paste
Tapping a tile calls one method:
textDocumentProxy.insertText(snippet.text)
The text goes into the document through the proxy, not through the pasteboard, so whatever the user copied earlier is still on their clipboard. The extension never reads from the proxy either. It has no letter keys, so it has no use for the text around the cursor, and documentContextBeforeInput is never called.
The globe key
App Review guideline 4.4.1 requires a keyboard to offer a way to the next one. The catch is that the system sometimes draws that key for you. On an iPhone with Face ID, iOS puts its own globe below your view and needsInputModeSwitchKey returns false; a second globe would be a duplicate. On an iPad it returns true, and without your own key the user is stuck.
nextKeyboardButton.addTarget(
self,
action: #selector(handleInputModeList(from:with:)),
for: .allTouchEvents
)
// in viewWillAppear, because the answer can change between appearances
nextKeyboardKey.isHidden = !needsInputModeSwitchKey
.allTouchEvents rather than .touchUpInside is what lets the system turn a long press into the keyboard picker and a tap into a plain switch.
Height is yours to get wrong
A keyboard extension sets its own height with a constraint, and nothing stops you from choosing badly. Mine was a hardcoded 260 points while the parts inside added up to 208; the extra 52 pushed the top edge over the host app's text field. Now the height is derived from the tiles, follows Dynamic Type, and is capped at 45% of the screen, after which the tiles scroll.
On iPad the keyboard can be floated, which changes its width without creating a new view. A flow layout keeps the cell sizes it measured first, so the layout has to say when to measure again:
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
newBounds.width != collectionView?.bounds.width
}
What it costs
- The keyboard can't sync, fetch or report anything. Sync would have to live in the app.
- It can't write back to the app. Any feature that wants the keyboard to remember something for the app needs a different design.
- The user has to add the keyboard in Settings by hand, and no API will do it for them. The app can only explain the steps.
Was it worth it
For this kind of keyboard, yes. "We don't collect what you type" is a promise. "This keyboard has no network access, and you can check that in Settings" is a property of the system. The second is easier to say, and easier to believe.
Light Clipboard is on the App Store, free for three snippets, for iPhone and iPad.
Get Light Clipboard