Until iOS 8, your ability to create a custom keyboard in iOS was very limited. Your only option was to create a custom input view to UITextField
and UITextView
, which meant you could only use your custom keyboard from within your own application.
Then like a hero appearing over the horizon, app extensions came to the rescue! These give you the ability to provide content to apps outside of your own. Keyboard extensions specifically allow your custom interfaces to provide input text to any app the user likes.
In this tutorial, you’ll walk through creating a custom keyboard extension that can be used by other apps.
Keyboard Extension Concepts
Custom keyboard extensions let users add additional keyboards to the list of available keyboards. A great example of an additional keyboard is the emoji keyboard built-in to iOS. If you enable this keyboard, you can tap the globe in the lower left corner of the default system keyboard to switch to it (assuming it’s the next one available).
A custom keyboard can fully replace the system keyboard, or act as a supplement like the emoji keyboard.
Your custom keyboard extensions provide UI that handles user interactions, such as button taps. They convert these actions into Strings
and send them to the host text input view. This gives you the freedom to use any of the iOS supported unicode characters.
But just because you added a custom keyboard doesn’t mean it will always be available. Here are some examples where a custom keyboard might be forbidden:
- Secure text input fields when the
secureTextEntry
is set totrue
. - Text input with the keyboard types
UIKeyboardTypePhonePad
andUIKeyboardTypeNamePhonePad
due to carrier character limitations. - An app author declines the use of keyboard extensions through the
AppDelegate
methodapplication(_:shouldAllowExtensionPointIdentifier:)
.
Managing User Expectations
Your success as a keyboard maker depends on your ability to meet the high expectation of users. There’s no room for error with keyboards. Most users have learned to expect certain functionality from their Apple devices, and the best way to understand the subtleties behind keyboard design is to study and play around with the system keyboard.
You’ll find that the default iOS keyboard is responsive, clean, responds dynamically depending on the type of text entry, provides autocorrection and text replacement, and just works.
These are just some of the expectations your users will have. Your job is to meet or exceed those expectations. Remember that the keyboard should never get in the way of what the user is trying to do, and you’ve got most of the UX solved right there.
Requirements
Just like standard iOS apps, there are certain requirements that your keyboard must fulfill before Apple will approve it:
- Next Keyboard Button: All keyboards require a way to switch to the next keyboard in the user’s enabled keyboards list. On the system keyboard, the globe key performs this action. It’s recommended to place this key in the bottom left-hand corner, as the system keyboard does, for consistency.
- App Purpose: Every keyboard extension must come bundled within a host app. This app must actually serve a purpose and provide the user with acceptable functionality. Yes, that measurement is subjective, and yes, that means you’ll actually have to put some time and thought into your host app!
- Trust: Constant news of data leaks has made users extremely sensitive about their data. As one of the main points where data flows into your apps, keyboards are a potentially vulnerable place for user data. It’s the keyboard author’s responsibility to ensure the safety of users’ keystroke data. Don’t unintentionally create a key-logger!
With all that background information, you’re probably itching to get your hands dirty.
Getting Started
In this tutorial, you’ll create a Morse Code keyboard. And don’t worry if you’re not already fluent in Morse code… because you will be by the end of this tutorial!
Here’s the scenario: you’ve created an awesome app for teaching people Morse code, but they can only practice while using your app. Now that you’ve learned that keyboard extensions exist, you’ve decided to provide the same great functionality to your users outside of the app!
Download the materials for this project and open the starter project in Xcode; you can find the link at the top or bottom of this tutorial. This is the host app you’ll use to deliver a keyboard extension.
Build and run the starter app. You’ll see the Morse code trainer screen:
This app lets the user practice Morse code using a custom keyboard and cheat sheet.
Morse code uses a series of “dot” and “dash” signals to represent letters. This implementation shows you the current series of signals and the number or letter it represents within the gray bar above the keyboard.
Try tapping combinations of dots and dashes to make letters. To complete a letter and move on to the next one, tap the space key. To enter an actual space, tap the space key twice.
After you’re comfortable with the keyboard functionality, you might be thinking, “My work here is done!” That would be an awesome tutorial, but unfortunately you can only use this keyboard within the sample app — and that defeats the whole purpose of a custom keyboard!
To better understand what’s already provided in the host app and what’s left to do in the keyboard extension, expand the folders in the Project navigator and look around:
Here’s a quick summary of what the files do:
- PracticeViewController.swift: Sets up the demo app UI and functionality.
- MorseColors.swift: Keyboards should support color schemes for both a light and dark display. This file defines color schemes for each type.
- MorseData.swift: Provides a way to convert from a series of “dots” and “dashes” to the equivalent letter and vice versa.
- KeyboardButton.swift: A
UIButton
subclass to customize the look and feel of keyboard keys. - MorseKeyboardView.swift / MorseKeyboardView.xib: A custom
UIView
that handles the layout and functionality of a Morse keyboard. This is where all the Morse-specific logic resides. It declares the protocolMorseKeyboardViewDelegate
to notify its delegate to perform certain actions on the text input view. This will allow you to useMorseKeyboardView
as both a keyboard input view as well as a keyboard extension.
Now that you’re familiar with the containing app, it’s time to make a keyboard extension!
Creating a Keyboard Extension
To create a keyboard extension on an existing project, go to File ▸ New ▸ Target…, select iOS and choose Custom Keyboard Extension. Click Next and name your keyboard MorseKeyboard. Make sure you select MorseCoder for Embedded in Application.
Click Finish to create your extension. If you get a popup to active the scheme, select Activate.
Xcode creates a new folder in the Project navigator for the extension. Expand MorseKeyboard to reveal its contents.
Take a look at each of the created keyboard extension files:
- KeyboardViewController.swift: A
UIInputViewController
subclass that acts as the primary view controller for custom keyboard extensions. This is where you’ll connect theMorseKeyboardView
and implement any custom logic for the keyboard similar to how it’s done inPracticeViewController
. - Info.plist: A
plist
that defines the metadata for your extension. TheNSExtension
item contains keyboard specific settings. You’ll cover the importance of this item later in the tutorial.
And that’s it. Two files is all you need to get going with a keyboard extension!
Open KeyboardViewController.swift to get a closer look at the generated template. You’ll notice a lot of code within viewDidLoad()
. As mentioned earlier, the main requirement of a custom keyboard is that it provides a key to switch to other keyboards.
This code programmatically adds a UIButton
to the keyboard with an action of handleInputModeList(from:with:)
. This method provides two different pieces of functionality:
- Tapping the button will switch to the next keyboard in the list of user-enabled keyboards.
- Long-pressing the button will present the keyboard list.
Time to run and test the keyboard! Unfortunately, running and debugging a keyboard extension is a little more involved than a typical app.
Enabling a Custom Keyboard
Select the MorseKeyboard scheme from the scheme selector at the top of Xcode.
Build and run the scheme, and you’ll be able to choose which app to embed it in. For your purposes, select Safari since it has a text field for you to use, and click Run.
After Safari loads, minimize the app (Press Command + Shift + H if on a simulator). Open the Settings app and go to General ▸ Keyboard ▸ Keyboards and select Add New Keyboard…. Finally, select MorseCoder.
Fortunately, you only need to add the keyboard once to each of your devices. Close the Settings app and open Safari. Select the address bar to present the system keyboard.
Once you see the system keyboard, long-press on the globe key. Select the newly added MorseKeyboard.
And there you have it: your very lacklustre keyboard! If you tap or long-press the Next keyboard button, you can switch back to the system keyboard.
That’s enough of this basic functionality. It’s time to join the keyboard big leagues with some custom UI!
Customizing Keyboard UI
Rather than having to recreate the UI in both your container app and extension, you can share the same code across both targets.
In the Project navigator, Command-click the following six files to select them:
- MorseColors.swift
- MorseData.swift
- KeyboardButton.swift
- MorseKeyboardView.swift
- MorseKeyboardView.xib
- Assets.xcassets
Now open the File inspector and add the selected files to the MorseKeyboard target:
This will add these files to both compilation targets, the freestanding MorseCoder app and the MoreKeyboard app extension. This is perhaps the simplest way to share code — use exactly the same source code file twice, for different targets which create different build products.
Now that your keyboard extension has access to these files, you can reuse the same UI. Open KeyboardViewController.swift and replace the entire contents of KeyboardViewController
with:
// 1
var morseKeyboardView: MorseKeyboardView!
override func viewDidLoad() {
super.viewDidLoad()
// 2
let nib = UINib(nibName: "MorseKeyboardView", bundle: nil)
let objects = nib.instantiate(withOwner: nil, options: nil)
morseKeyboardView = objects.first as! MorseKeyboardView
guard let inputView = inputView else { return }
inputView.addSubview(morseKeyboardView)
// 3
morseKeyboardView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
morseKeyboardView.leftAnchor.constraint(equalTo: inputView.leftAnchor),
morseKeyboardView.topAnchor.constraint(equalTo: inputView.topAnchor),
morseKeyboardView.rightAnchor.constraint(equalTo: inputView.rightAnchor),
morseKeyboardView.bottomAnchor.constraint(equalTo: inputView.bottomAnchor)
])
}
KeyboardViewController
now only contains code to set up the keyboard view. Here’s what you created:
- A property to hold reference to a
MorseKeyboardView
object. - An instance of
MorseKeyboardView
is added to the controller’s rootinputView
. - Constraints pinning
morseKeyboardView
to the superview are added and activated.
Rather than building out the UI from scratch, you’re simply reusing the same code from the host app. Build and run the keyboard extension through Safari. Select the address bar and long-press the globe key to select MorseKeyboard.
Things are looking a little better: The keys are clickable, and the correct letter is being shown within the keyboard preview, but the address bar isn’t updating.
Note: You’ll notice that the height of the keyboard is different from that of the system keyboard. Keyboard extensions automatically infer their height based on the nib’s Auto Layout constraints.
Things look great, but something’s missing: the mandatory next keyboard button!
Open MorseKeyboardView.xib and you’ll see there’s already a next keyboard globe added to the nib.
Hmm… it looks like something’s hiding the next keyboard key. Open MorseKeyboardView.swift and look at the method setNextKeyboardVisible(_:)
.
This is a custom method that actives and deactivates certain constraints to hide or show the next keyboard key. It exists because there are situations where you need to hide the key.
UIInputViewController
defines a property needsInputModeSwitchKey
that tells your custom keyboard whether or not it’s required to show a next keyboard key. An example of when this might be false
is when your keyboard is running on an iPhone X. This device provides a next keyboard key below a raised keyboard by default, so you don’t need to add your own.
You’ll use this property to control the visibility of the globe key. Open KeyboardViewController.swift and add the following line to the bottom of viewDidLoad()
:
morseKeyboardView.setNextKeyboardVisible(needsInputModeSwitchKey)
This tells the Morse keyboard whether or not to hide the next keyboard key based on the value of needsInputModeSwitchKey
.
To test this out, you’ll need to run the keyboard on multiple devices. Build and run on both an iPhone X and any other iPhone. Remember to add the keyboard through Settings ▸ General ▸ Keyboard ▸ Keyboards ▸ Add New Keyboard… if you’re running a new device.
You should see that the iPhone X hides your custom next keyboard key because there’s already a system globe key. You’ll also see your custom key as expected on the other device.
Right now your key doesn’t do anything, but frankly, neither do any of the other buttons. It’s time to fix that!
Attaching Keyboard Actions
Just like the template, you’ll add an action to the globe key programmatically. Add the following code to the end of viewDidLoad()
:
morseKeyboardView.nextKeyboardButton.addTarget(self,
action: #selector(handleInputModeList(from:with:)),
for: .allTouchEvents)
This adds handleInputModeList
as an action to the next keyboard key which will automatically handle switching for you.
All of the remaining MorseKeyboardView
keys have already been attached to actions within the view. The reason they currently aren’t doing anything is because you haven’t implemented a MorseKeyboardViewDelegate
to listen for these events.
Add the following extension to the bottom of KeyboardViewController.swift:
// MARK: - MorseKeyboardViewDelegate
extension KeyboardViewController: MorseKeyboardViewDelegate {
func insertCharacter(_ newCharacter: String) {
}
func deleteCharacterBeforeCursor() {
}
func characterBeforeCursor() -> String? {
return nil
}
}
MorseKeyboardView handles all of the Morse-related logic and will call different combinations of these methods after the user taps a key. You’ll implement each of these method stubs one at a time.
insertCharacter(_:)
tells you that it’s time to insert a new character after the current cursor index. In PracticeViewController.swift, you directly add the character to the UITextField
using the code textField.insertText(newCharacter)
.
The difference here is that a custom keyboard extension doesn’t have direct access to the text input view. What you do have access to is a property of type of UITextDocumentProxy
.
A UITextDocumentProxy
provides textual context around the current insertion point without direct access to the object – that’s because it’s a proxy.
To see how this works, add the following code to insertCharacter(_:)
:
textDocumentProxy.insertText(newCharacter)
This tells the proxy to insert the new character for you. To see this work, you’ll need to set the keyboard view’s delegate. Add the following code to viewDidLoad()
after assigning the morseKeyboardView
property:
morseKeyboardView.delegate = self
Build and run the keyboard extension in Safari to test this new piece of functionality. Try pressing different keys and watch as gibberish appears in the address bar.
It’s not exactly working as expected, but that’s only because you haven’t implemented the other two MorseKeyboardViewDelegate
methods.
Add the following code to deleteCharactersBeforeCursor()
:
textDocumentProxy.deleteBackward()
Just as before with insert, you’re simply telling the proxy object to delete the character before the cursor.
To wrap up the delegate implementation, replace the contents of characterBeforeCursor()
with the following code:
// 1
guard let character = textDocumentProxy.documentContextBeforeInput?.last else {
return nil
}
// 2
return String(character)
This method tells MorseKeyboardView
what the character before the cursor is. Here’s how you accomplished this from a keyboard extension:
textDocumentProxy
exposes the propertydocumentContextBeforeInput
that contains the entire string before the cursor. For the Morse keyboard, you only need the final letter.- Return the
Character
as aString
.
Build and run the MorseKeyboard scheme and attach it to Safari. Switch to your custom keyboard and give it a try. You should see the correct letter for the pattern you type show up in the address bar!
Up to this point you’ve been indirectly communicating with the text input view, but what about communicating in the reverse direction?
Responding to Input Events
Because UIInputViewController
implements the UITextInputDelegate
, it receives updates when certain events happen on the text input view.
Note: Unfortunately, at the time of writing, despite many years having passed since this functionality was added to UIKit, not all these methods function as documented.
Caveat coder.
You’ll focus on the textDidChange(_:)
delegate method and how you can use this within your keyboard extensions. Don’t let the name fool you – this method is not called when the text changes. It’s called after showing or hiding the keyboard and after the cursor position or the selection changes. This makes it a great place to adjust the color scheme of the keyboard based on the text input view’s preference.
In KeyboardViewController
, add the following method implementation below viewDidLoad()
:
override func textDidChange(_ textInput: UITextInput?) {
// 1
let colorScheme: MorseColorScheme
// 2
if textDocumentProxy.keyboardAppearance == .dark {
colorScheme = .dark
} else {
colorScheme = .light
}
// 3
morseKeyboardView.setColorScheme(colorScheme)
}
This code checks the text input view’s appearance style and adjusts the look of the keyboard accordingly:
MorseColorScheme
is a custom enum type defined in MorseColors.swift. It defines adark
andlight
color scheme.- To determine what color scheme to use, you check the
textDocumentProxy
propertykeyboardAppearance
. This provides adark
andlight
option as well. - You pass the determined
colorScheme
tosetColorScheme(_:)
on the Morse keyboard to update its scheme.
To test this out, you’ll need to open the keyboard in a text input with a dark mode. Build and run the extension in Safari. Minimize the app and swipe down to show the search bar.
Notice that the default keyboard is now dark. Switch to the Morse keyboard.
Voilà! Your custom keyboard can now adapt to the text input view that presented it! Your keyboard is in a really great spot… but why stop there?
Autocorrection and Suggestions
Adding autocorrection and suggestions similar to those of the system keyboard fulfills a common expectation of keyboards. If you do this, however, you’ll need to provide your own set of words and logic. iOS does not automatically provide this for you.
It does, however, provide you with a way to access the following device data:
- Unpaired first and last names from the user’s Address Book.
- Text shortcuts defined in the Settings ▸ General ▸ Keyboard ▸ Text Replacement.
- A very limited common words dictionary.
iOS does this through the UILexicon
class. You’ll learn how this works by implementing auto text replacement on the Morse keyboard.
In KeyboardViewController
, add the following under the declaration for morseKeyboardView
:
var userLexicon: UILexicon?
This will hold the lexicon data for the device as a source of words to compare against what the user typed.
To request this data, add the following code to the end of viewDidLoad()
:
requestSupplementaryLexicon { lexicon in
self.userLexicon = lexicon
}
This method requests the supplementary lexicon for the device. On completion, you’re given a UILexicon
object that you save in the userLexicon
property you just defined.
In order to know if the current word matches one in the lexicon data, you’ll need to know what the current word is.
Add the following computed property under userLexicon
:
var currentWord: String? {
var lastWord: String?
// 1
if let stringBeforeCursor = textDocumentProxy.documentContextBeforeInput {
// 2
stringBeforeCursor.enumerateSubstrings(in: stringBeforeCursor.startIndex...,
options: .byWords)
{ word, _, _, _ in
// 3
if let word = word {
lastWord = word
}
}
}
return lastWord
}
Let’s break down how this code gets the current word based on the cursor location:
- You again use
documentContextBeforeInput
to get the text before the cursor. - You enumerate each word of the string by using
enumerateSubstrings
. - Unwrap
word
and save it inlastWord
. When this enumeration ends, whateverlastWord
contains will be the last word before the cursor.
Now that you have the currently typed word, you’ll need a place to do the autocorrection or replacement. The most common place for this is after pressing the Space key.
Add the following extension to the end of the file:
// MARK: - Private methods
private extension KeyboardViewController {
func attemptToReplaceCurrentWord() {
// 1
guard let entries = userLexicon?.entries,
let currentWord = currentWord?.lowercased() else {
return
}
// 2
let replacementEntries = entries.filter {
$0.userInput.lowercased() == currentWord
}
if let replacement = replacementEntries.first {
// 3
for _ in 0..<currentWord.count {
textDocumentProxy.deleteBackward()
}
// 4
textDocumentProxy.insertText(replacement.documentText)
}
}
}
This is a good chunk of code, so let's go through it step by step:
- Ensure that the user lexicon and current word exist before continuing.
- Filter the lexicon data by comparing
userInput
to the current word. This property represents the word to replace. An example of this is replacing "omw" with "On my way!". - If you find a match, delete the current word from the text input view.
- Insert the replacement text defined using the lexicon property
documentText
.
And that's it! To call this method after entering a space, add the following code to the top of insertCharacter(_:)
before the call to insertText(_:)
:
if newCharacter == " " {
attemptToReplaceCurrentWord()
}
This code makes sure to only perform a text replacement after entering the literal space character. This avoids replacing text while pressing the Space key to start a new letter.
Give this new functionality a try! Build and run the keyboard target in Safari. Type "omw" and then press Space twice.
Cheat Sheet:
– – – space – – space • – – space space
You should see the letters "omw" replaced by "On my way!". This happens because iOS automatically adds this as a text replacement. You can play around with this functionality by adding more words to Settings ▸ General ▸ Keyboard ▸ Text Replacement or by running this on your phone and typing a name from your contacts.
Although this only works with a limited scope of words, it's essential for a custom keyboard to provide the user with this functionality as users have learned to expect this from the system keyboard.
With that, you've wrapped up the basic and intermediate functionality of a keyboard extension... but what if you want more?
Requesting Open Access
In order to do more advanced things from a keyboard extension, you'll need to request those privileges from the user. Open Access is a permission that the user can choose to allow or disallow. It gives your extension a number of capabilities, including:
- Location Services and Address Book, including names, places, and phone numbers.
- Keyboard and containing app can employ a shared container which allows features like iCloud and In-App Purchases.
- Network access for connecting with web services.
- Ability to edit keyboard’s custom autocorrect lexicon via the containing app.
But as Uncle Ben once said, "with great power comes great responsibility." It's up to you to safely handle this sensitive user data.
To get a taste of this power, you'll request access to the user's location from the keyboard extension. Why would a keyboard need the user's location? Well, Morse code is most commonly associated with SOS messages when in distress. It would be great to automatically insert the current location after typing "SOS"!
The first thing you'll need is the user's location. You'll use the Core Location framework to do this. Add the following import to the top of KeyboardViewController.swift:
import CoreLocation
Next, add a property to store current user location under currentWord
:
var currentLocation: CLLocation?
To set this property, you'll use the CLLocationManagerDelegate
protocol. Add the following extension to the bottom of the file:
// MARK: - CLLocationManagerDelegate
extension KeyboardViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
currentLocation = locations.first
}
}
Updates to the user's location trigger a call to this method. To start location updates you'll need to create a CLLocationManager
instance. Add the following property under currentLocation
:
let locationManager = CLLocationManager()
To have locationManager
start updating the location, you'll have to set up some properties. Add the following code to the end of viewDidLoad()
:
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 100
locationManager.startUpdatingLocation()
This tells the location manager object to only generate location updates while the keyboard is in use.
Note: For more on using CoreLocation to access location data, check out the Apple documentation.
The last thing to do is insert the current location after typing the "SOS" message. Replace the current definition of insertCharacter(_:)
with this new definition:
func insertCharacter(_ newCharacter: String) {
if newCharacter == " " {
// 1
if currentWord?.lowercased() == "sos",
let currentLocation = currentLocation {
// 2
let lat = currentLocation.coordinate.latitude
let lng = currentLocation.coordinate.longitude
textDocumentProxy.insertText(" (\(lat), \(lng))")
} else {
// 3
attemptToReplaceCurrentWord()
}
}
textDocumentProxy.insertText(newCharacter)
}
Here's what this does:
- You check if the current word is "sos" and that you have the user's location.
- You insert the current latitude and longitude of the user into the text input view.
- If the current word isn't "sos" or you have no location information, then do as you did before and attempt to replace the current word.
It's time to see if your old-school-distress-signal-meets-modern-tech mechanism works. Build and run the keyboard extension in Safari and type out "sos".
Cheat Sheet:
• • • space – – – space • • • space space
Andddd nothing... That's a bummer. Check the console, and you'll see something like this:
Of course! You're missing some privacy settings in the keyboard property list.
Open Info.plist from within the MorseKeyboard folder. Select Bundle version and click the + symbol to add a new key-value pair.
Type NSLocationWhenInUseUsageDescription for the key and use whatever user friendly message you'd like for requesting access as the value.
Note: At this point, you could technically run the extension on the simulator and see a successful SOS location. There's a bug on the iOS Simulator where you're able to access this data without requesting open access.
Next, expand NSExtension and then NSExtensionAttributes to see the options available to your keyboard extension.
Each of these settings are pretty self explanatory, but if you'd like to learn more about them, check out the Apple documentation
To request open access to get user location data, change the value of RequestsOpenAccess to YES. This means that when installing the keyboard, your users can decide whether to allow access.
Since you've already installed the keyboard, you'll have to give it access manually. Build and run the extension to push these latest changes to your device.
Minimize the app and go to Settings ▸ General ▸ Keyboard ▸ Keyboards ▸ MorseKeyboard and switch the toggle on. Tap Allow on the alert to give full access to your keyboard.
Build and run the extension in Safari one last time. After switching to the keyboard you'll be prompted to allow access to location data. Tap Allow and give "sos" another try.
And there it is! The keyboard is successfully accessing your current location and inserting it into the text input view!
Where To Go From Here?
You can download the final project using the link at the top or bottom of this tutorial.
With this newfound keyboard extension knowledge, you're well on your way to making something truly useful for the world.
This only scratches the surface of all the functionality you could add to a keyboard extension. If you want a deeper dive, check out the Apple Custom Keyboard documentation and Chapter 14 of iOS 8 by Tutorials.
Thanks for following along. As always, if you have any questions or comments on this tutorial, feel free to join the discussion below!
The post Custom Keyboard Extensions: Getting Started appeared first on Ray Wenderlich.