Developing a Custom Keyboard (Custom Keyboard) for iOS
Custom Keyboard Extension is one of the few iOS extension types that access input across all apps simultaneously. This same capability makes it the most complex to pass App Store review: Apple carefully checks RequestsOpenAccess compliance and actual behavior.
Where Everything Breaks Before Release
Architecturally, Custom Keyboard works via UIInputViewController. It's not just a view controller—it lives in a separate process isolated from the host app. No access to UserDefaults with suite, no shared keychain without App Groups, no network without RequestsOpenAccess: YES in Info.plist.
First typical error: developers set up App Group to share settings from the main app to the extension but forget to add the group to the Extension target's Capabilities—not just the main app. Xcode doesn't warn. Crash comes on device as nil when reading UserDefaults(suiteName:).
Second problem is more serious. Apple requires that if RequestsOpenAccess is set to YES, the keyboard must explicitly describe in Privacy Policy how input data is used. If the description is vague or missing—rejection on 5.1.1 (Data Collection and Storage). If the flag is NO, network requests silently fail without any log errors.
Keys to Track in Extension's Info.plist
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IsASCIICapable</key>
<false/>
<key>PrefersRightToLeft</key>
<false/>
<key>PrimaryLanguage</key>
<string>en-US</string>
<key>RequestsOpenAccess</key>
<true/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.keyboard-service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).KeyboardViewController</string>
</dict>
How a Proper Custom Keyboard Works
UIInputViewController provides textDocumentProxy—a UITextDocumentProxy object through which the keyboard interacts with the host app's text field. Insert text: textDocumentProxy.insertText("a"). Delete character: textDocumentProxy.deleteBackward(). Switch language: advanceToNextInputMode(). Close keyboard: dismissKeyboard().
Important: textDocumentProxy.documentContextBeforeInput and documentContextAfterInput return text around cursor—but not always. In some fields (protected text fields, attributes like isSecureTextEntry), proxy returns nil. Handle this explicitly, otherwise any autocorrect or word prediction logic breaks on password fields.
Keyboard Height Problem
System keyboard auto-adapts to Safe Area. Custom keyboards don't. Set height via heightConstraint on inputView and recalculate on orientation change:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let newHeight: CGFloat = view.bounds.width > view.bounds.height ? 180 : 256
if heightConstraint?.constant != newHeight {
heightConstraint?.constant = newHeight
view.layoutIfNeeded()
}
}
Without explicit recalc, on iPhone in landscape the keyboard either cuts off or covers content at wrong height.
State Persistence Between Sessions
Extension has no persistent memory—the process terminates with input. Store settings (layout, theme, autocorrect) in UserDefaults via App Group. Larger data (trained dictionary, emoji history)—in shared CoreData container or file in shared container via FileManager.containerURL(forSecurityApplicationGroupIdentifier:).
Testing and Review
Testing Custom Keyboard in the simulator is inconvenient: keyboard switching works differently than on real device. On simulator, advanceToNextInputMode() often doesn't trigger. Use real device with TestFlight from the start.
Before submitting to review: test behavior in UISearchBar, UITextView with isEditable = false, in Safari Address Bar (extension doesn't activate there—normal, but keyboard shouldn't crash).
App Store review separately checks: no hidden keylogging, Privacy Policy matches, actual functionality matches claims.
Timeline: 1–3 weeks depending on custom lexicon, autocorrect, multilingual support. Cost calculated individually.







