Custom Keyboard Extensions: Getting Started

Custom keyboard extensions give you the ability to provide keyboards to apps outside of your own. In this tutorial, you’ll walk through creating a custom keyboard extension with advanced features like autocomplete. By Eric Cerney.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. textDocumentProxy exposes the property documentContextBeforeInput that contains the entire string before the cursor. For the Morse keyboard, you only need the final letter.
  2. Return the Character as a String.

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:

  1. MorseColorScheme is a custom enum type defined in MorseColors.swift. It defines a dark and light color scheme.
  2. To determine what color scheme to use, you check the textDocumentProxy property keyboardAppearance. This provides a dark and light option as well.
  3. You pass the determined colorScheme to setColorScheme(_:) 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:

  1. You again use documentContextBeforeInput to get the text before the cursor.
  2. You enumerate each word of the string by using enumerateSubstrings.
  3. Unwrap word and save it in lastWord. When this enumeration ends, whatever lastWord 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:

  1. Ensure that the user lexicon and current word exist before continuing.
  2. 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!".
  3. If you find a match, delete the current word from the text input view.
  4. 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?

Eric Cerney

Contributors

Eric Cerney

Author

Alexis Gallagher

Tech Editor

Chris Belanger

Editor

Jeff Rames

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.