By

UITextField Keyboard Type Returns Initial Value - No Matter What

The problem

Let’s say you have a text field and you want to know in which state the keyboard is i.e. either letter mode or number mode. Well, ask the text field right? It will return an UIKeyboardType integer.

UIKeyboardType keyboardType = self.textField.keyboardType;

The problem begins as soon as the user manually changes the keyboard and you want to know which keyboard type is shown. The surprise is that the text field still returns the initially set keyboard type. At first I thought that this was a bug, but essentially the text field is not lying since the text field always wants the previously set keyboard type as a default. If the user leaves the text field and enters the same text field again, we expect the text field to be in that initial state. So my suggestion would be that Apple should look at improving the API for accessing the keyboard directly. For example we should rather ask the keyboard for its state than the text field.

UIKeyboard *keyboard;
UIKeyboardType keyboardType = keyboard.keyboardType;

As you know unfortunately that’s not the case.

The project I am currently working on relies on that state as follows. The keyboard return key needs to change from Done to Search if at least one character is written into the text field. In order to change the return keys you need to call an update on the text field.

if (self.textField.text.length == 0)
{
    self.textField.returnKeyType = UIReturnKeyDefault;
}
else
{
    self.textField.returnKeyType = UIReturnKeySend;
}
[self.textField reloadInputViews];

The side effect is that the keyboard jumps back to the keyboardType which was initially set for the text field… and there’s the problem. A user switches from letters to numbers and enters a number. Now the text field’s text has one character, the keyboard return key needs to update. The keyboard is updated and, as you might expect, the keyboard jumps magically back from number keyboard to the initial letter type. Uncool.

How I came up with one (of probably many) solution

To help me understand this, I printed out the subviews of the keyboard to find hints of the type of keyboard in the console log. Looking through the logs I then found some suspicious views, which in their frame dimension looked like the keyboard type switch button. From reading the text on the button, I could then see if it says 123 it must be in letter mode and if it says ABC it must be in number mode. To make use of this I now had to guess which property is used to store the label text on the view. Runtime headers to the rescue!

https://github.com/nst/iOS-Runtime-Headers

That solution worked, but obviously that is not nice for localised keyboards.

How I came up with a better solution

The same project I was talking about also uses Calabash for testing the UI automated. They obviously can touch on the keyboard and change it from letters to numbers. How does Calabash know where to “touch”? After deep diving into Calabash iOS I found the possible culprits. UIKBKeyplane and UIKBTree. The following ruby code file keyboard_helpers.rb from Calabash shows that they iterate through the keyboard view hierarchy and try to find a UIKBKeyplane object and within it a UIKBTree object. Consulting the runtime headers again you find a lot of useful info as well.

UIKBKeyplaneView: https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UIKBKeyplaneView.h

UIKBTree: https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UIKBTree.h

There is a string componentName which looks suspicious and is also used by Calabash iOS. Having that info I tried to go through the subviews of the keyboard again recursively and wait to find a UIKBKeyplane and within the keyplane’s subviews a UIKBTree which contains the readonly value componentName.

Success! componentName returns strings like "Capital-Letters" and "Numbers-And-Punctuation". In order to get the UIKeyboardType I created a dictionary to be able to map those strings.

self.mapDictionary = @{
    @"Capital-Letters":
        @(UIKeyboardTypeASCIICapable),
    @"Small-Letters":
        @(UIKeyboardTypeASCIICapable),
    @"First-Alternate":
        @(UIKeyboardTypeASCIICapable),
    @"Numbers-And-Punctuation":
        @(UIKeyboardTypeNumbersAndPunctuation),
    @"Numbers-And-Punctuation-Alternate":
        @(UIKeyboardTypeNumbersAndPunctuation)
};

From now on I was able to ask the keyboard, not the text field, for its current keyboard type. So back to my problem - before calling reloadInputViews I now ask the keyboard for the state and override the text field’s value. That results in recreating a keyboard with the previous keyboard type. To see the full solution go to GitHub.

Contribute

I have pushed a pod named US2KeyboardType. The source is on GitHub. Feel free to use the pod or even contribute with optimisations.

If you know an easier solution or how to optimise the approach, let me know.

Written by
I'm an iOS developer, focusing on beautiful apps and code.