We are developing a habraklaviatura for iOS

  • Tutorial

Often, to read the Habr, I use the Habrahabr mobile application for iPhone and iPad. It is convenient enough for reading articles, but not very convenient for writing comments, especially if you want to write something like that using formatting tags. Inconvenient, because all tags must be typed manually, so it’s very easy to make a mistake and, as a result, leave an ugly comment.

So I had an idea to write my own keyboard, in which, by pressing a key, an opening and closing tag is added to the text field. In this case, the cursor should be directly between them in order to immediately start writing text. It is also necessary to be able to move the cursor using swipe gestures, subjectively, it is more convenient than pulling a finger to the field, waiting for a magnifying glass to appear, moving your finger and hoping that the cursor gets where it is needed. And finally, it's time to deal with the Sarcasm and Bore tags, which are not supported by the Habr parser. The keyboard should have special keys for these purposes, and the design of tags should be configurable in the keyboard settings so that everyone can specify the look that he likes.

With the release of iOS 8, Apple opens a new API that allows you to develop extensions to applications. Keyboard (Custom Keyboard) is one of the representatives of such extensions. It will be discussed. In the article, you will learn about the features, limitations, and bugs that the new API provides, how to develop a habraclav keyboard, and how to make your keyboard appear in the AppStore, and therefore on the devices of your users.

Features and limitations


Having opened access to the API for creating third-party keyboards, Apple has built a narrow bridge between applications. On the one hand, each application is still in its own sandbox, but on the other, the entered data in one application can now go to another, or directly sent to the server. Such functionality is quite serious in terms of user data security, so Apple has strictly defined what can and cannot be done. Before proceeding to the detailed description, I want to clarify that all types of extensions, including the keyboard, can be installed on a mobile device only as part of the main application (container application). For example, a widget extension was added to the latest version of the Habrahabr application in the Notification Center.

And so, what opportunities do we have:
  • A third-party keyboard can be used in almost any application installed on the device. To do this, the user must manually add the installed keyboard in the device settings. Fortunately, application developers can prohibit the use of third-party keyboards in their applications, but more on that later;
  • The keyboard can exchange data with both the container application and the developer server. The permission of such an exchange is user-configurable, and this feature was initially disabled;
  • The keyboard may have access to the geolocation service and address book. To gain access, user permission is required both in the keyboard settings and in the settings of the corresponding service;
  • The keyboard can access the built-in dictionary to display options for auto-correction and completion of the entered text. Data is taken from the following sources:
    1. Device Address Book (first and last names are provided unpaired);
    2. Abbreviations specified by the user in the settings of the smartphone;
    3. General vocabulary.

These, in fact, are all the main features. Let's move on to the limitations:
  • Cannot be inherited from a standard keyboard. That is, taking the built-in keyboard as a basis and adding gesture processing to control the cursor will not work, you will have to do everything from scratch. Moreover, a standard keyboard cannot be recreated in principle. The reasons are described in the following paragraphs;
  • A third-party keyboard, like other types of extensions, does not have access to a microphone, which makes it impossible to support voice input;
  • A third-party keyboard cannot be used to enter hidden text ( secureTextEntry = YES). That is, if the field is intended for entering a password, then the user can use only the standard keyboard;
  • Custom keyboard cannot be used for fields with type UIKeyboardTypePhonePadand UIKeyboardTypeNamePhonePad;
  • The keyboard does not have access to highlight text in the input field. That is, changing the selection without user intervention does not work;
  • Unlike a standard keyboard, a third-party one cannot get out of the frame. That is, you cannot display anything above the keyboard, as, for example, with a long press on the keys of the top row of the standard keyboard;
  • Developers may prohibit the use of third-party keyboards in their applications. To do this, you must override the application:shouldAllowExtensionPointIdentifier:protocol method UIApplicationDelegate. For an identifier with a name, UIApplicationKeyboardExtensionPointIdentifieryou must return NO. By the way, for iOS 8, this is the only type of extension that you can forbid to use.

Complete Custom Keyboard Development Documentation: Custom Keyboard

Habraclavatar


We’ve sorted out the theory, let's move on to practice.
We create a new project, select Application, everything is standard.


Next, you need to add a new Target “Custom Keyboard”.


As a result, Xcode generates a class - an inheritor from UIInputViewControllerand Info.plist.
The class UIInputViewControlleris a keyboard controller. All interaction with the input field occurs through it. Consider the class interface in more detail.
The main methods:
  • - (void)dismissKeyboard- allows you to hide the keyboard. This is an opportunity that is missing in all standard keyboards on the iPhone;
  • - (void)advanceToNextInputMode- Displays the next keyboard. The list of available keyboards is determined by the user in the device settings;
  • - (void)requestSupplementaryLexiconWithCompletion:(void (^)(UILexicon *))completionHandler- Provides an array of pairs of strings. Each pair consists of a line that the user can enter userInputand a line that is auto-completion or auto-correction documentText. For example, on my iPhone, this method returns 151 pairs.

A property is provided to interact with the field textDocumentProxy. I will give a description of only the most important methods for development:
  • - (void)adjustTextPositionByCharacterOffset:(NSInteger)offset - allows you to control the cursor;
  • - (NSString *)documentContextBeforeInput - returns a line to the cursor;
  • - (NSString *)documentContextAfterInput - returns a line after the cursor;
  • - (void)insertText:(NSString *)text - inserts a line after the cursor;
  • - (void)deleteBackward - deletes one character in front of the cursor;
  • - (UIKeyboardAppearance)keyboardAppearance - allows you to determine which topic is used: light or dark;
  • - (UIKeyboardType)keyboardType - allows you to determine what type of keyboard the input field requires.

In addition to the methods described above, the class UIInputViewControllerimplements the protocol UITextInputDelegate:
@protocol UITextInputDelegate 
- (void)selectionWillChange:(id)textInput;
- (void)selectionDidChange:(id)textInput;
- (void)textWillChange:(id)textInput;
- (void)textDidChange:(id)textInput;
@end

Calls of these methods should report the selection and change of text in the input field, while the object textInputmust provide information about the input field itself and the text that it contains.
But in fact, we have the following behavior:
  • The first two methods are never called whether the user selects text or not;
  • The last two methods are called, but the object is textInputalways nil.

It looks like a bug. On Stackoverflow, people write that they faced the same problem, there is no solution. I want to note that the above described behavior is reproduced on the release version of iOS8.

The second common ground for the developer is the file Info.plist. In addition to the already known fields, it contains a group NSExtension:
NSExtensionNSExtensionAttributesIsASCIICapablePrefersRightToLeftPrimaryLanguageruRequestsOpenAccessNSExtensionPointIdentifiercom.apple.keyboard-serviceNSExtensionPrincipalClassVPKeyboardViewController

It indicates the type of extension being developed, the name of the controller class, and attributes. Pay attention to the attribute RequestsOpenAccess. With its help, the system understands whether you need advanced access: data exchange with a container application or server, access to geolocation and address book. If you indicate true, then be prepared to explain to Apple why you need all this.

This completes the familiarization with the API and proceed to direct development.
First, let's define a layout. I planned to implement portrait and landscape orientation support for the iPhone and eventually modify it for the iPad. The layout for portrait and landscape orientations should have been slightly different. For these purposes, the newly made Sizes Classes technology was perfectly suitable.. Why am I writing in the past tense? Yes, because all the plans failed. The fact is that regardless of the orientation, the system assigns us the same Size Classes: wCompact and hCompact, which corresponds to the landscape orientation for the iPhone. Most likely this is due to the fact that the keyboard frame does not occupy the entire screen, but only the lower half. In principle, this is a logical behavior, and to get around this problem, you can manually assign an arbitrary Size Class to the controller. To do this, use the method setOverrideTraitCollection:forChildViewController:. But there it was, in fact the call of this method does not affect anything, that isUITraitCollectionchild controller remains unchanged. If any of you have had a positive experience using this method, please share it. I uploaded the version of the code with the behavior described above into a separate brunch, if anyone is interested you can dig into it. Until the problem is resolved, we will be content with one layout for all orientations:


For convenience of cursor control, we add recognition of Swipe gestures. In xib we add two objects UISwipeGestureRecognizer, we implement event handlers in the code:
- (IBAction)onLeftSwipeRecognized:(id)sender {
    if (self.textDocumentProxy.documentContextBeforeInput.length > 0) {
        [self.textDocumentProxy adjustTextPositionByCharacterOffset:-1];
    }
}
- (IBAction)onRightSwipeRecognized:(id)sender {
    if (self.textDocumentProxy.documentContextAfterInput.length > 0) {
        [self.textDocumentProxy adjustTextPositionByCharacterOffset:1];
    }
}

Next, add handlers to close the keyboard and go to the next:
- (IBAction)onNextInputModeButtonPressed:(id)sender {
    [self advanceToNextInputMode];
}
- (IBAction)onDismissKeyboardButtonPressed:(id)sender {
    [self dismissKeyboard];
}

To delete the entered text, we implement two possibilities:
  1. Delete the last character before the cursor, as in a standard keyboard:
    - (IBAction)onDeleteButtonPressed:(id)sender {
        if (self.textDocumentProxy.documentContextBeforeInput.length > 0) {
            [self.textDocumentProxy deleteBackward];
        }
    }
    
  2. Deletes all entered text regardless of cursor position. To avoid accidentally deleting text, we will use UILongTapGestureRecognizer:
    - (IBAction)onClearButtonPressed:(id)sender {
        NSInteger endPositionOffset = self.textDocumentProxy.documentContextAfterInput.length;
        [self.textDocumentProxy adjustTextPositionByCharacterOffset:endPositionOffset];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // We can't know when text position adjustment is finished
            // Hack: Call this code after delay. In other case these changes won't be applied
            while (self.textDocumentProxy.documentContextBeforeInput.length > 0) {
                [self.textDocumentProxy deleteBackward];
            }
        });
    }
    

    To achieve the goal, you have to use a hack with delayed code execution. The fact is that the API allows you to delete text only before the cursor. That is, in order to delete all the text, you must first move the cursor to the end of the line, but the process of moving is asynchronous, at the same time I did not find the opportunity to find out the point in time when this process was completed. Therefore, we set the delay at 0.1 second and believe that the cursor has reached its goal.

It remains to figure out what we are actually here for: with the introduction of formatting tags.
To store the standard tags that are supported by the Habr, we will use the JSON file:
{
    "Жирный": "",
    "Курсив": "",
    "Подчеркнутый": "",
    "Зачеркнутый": "",
    "Цитата": "
", "Код": "", "Ссылка": "", "Картинка": "", "Видео": "", "Спойлер": "", "Хабраюзер": "" }


For the "Sarcasm" and "Bore" tags, it is necessary to create settings so that each user can set the values ​​for the opening and closing tags. Add Settings Bundle:


Go to Settings.bundle -> Root.plist and fill in all the necessary fields. Below is the source code of the settings and what the user should see:


But in reality, when installing the keyboard, the values ​​for the tags are not displayed, that is, in fact the fields are empty. These fields are set by key.Default Value. At first I thought that I was doing something wrong. But even if you go into the settings and manually fill in these fields, then when you exit the settings, the values ​​are not saved. This is a bug. Other users also faced a similar problem, several topics on Stackoverflow confirm this, that is, the problem is not local. It seems that the developers forgot to call the method synchronizeon the object NSUserDefaults. Sadly, one can only wait for the update of iOS 8.1 or iOS 8.0.1. To take this problem into account, I use the default values ​​in the code if the settings failed to load.

We’ve sorted out the storage of tags, now we’ll write a keystroke handler to add tags to the input field:
- (IBAction)onHabraButtonPressed:(id)sender {
    NSString *tagKey = [sender titleForState:UIControlStateNormal];
    NSString *tagValue = self.tagsDictionary[tagKey];
    [self.textDocumentProxy insertText:tagValue];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // We can't know when text insert is finished
        // Hack: Call this code after delay. In other case these changes won't be applied
        [self moveTextPositionToInputPointForTag:tagValue];
    });
}
- (void)moveTextPositionToInputPointForTag:(NSString *)tag {
    static NSArray *inputPointLabels = nil;
    if (inputPointLabels == nil) {
        inputPointLabels = @[@"]", @"://", @"=\"", @">"];
    }
    for (NSString *label in inputPointLabels) {
        NSRange labelRange = [tag rangeOfString:label];
        if (labelRange.location != NSNotFound) {
            NSInteger offset = labelRange.location + labelRange.length - tag.length;
            [self.textDocumentProxy adjustTextPositionByCharacterOffset:offset];
            break;
        }
    }
}

It uses the previously described hack with a delay in the execution of the code. This is due to the same cursor. When we call the method, the insertText:cursor does not move instantly, and this must be taken into account. To explain why you need to consider the cursor shift, I’ll give an example: let's say we want to add a link to some Habrauser. To do this, add a tag and then enter the username between the quotes. For convenience, I made the cursor automatically set to the position between the quotes. Similarly for other tags. For these purposes, the method described above is used moveTextPositionToInputPointForTag:, which, using an array of tag lines, determines the position at which the cursor must be positioned.

The implementation is completed, select the extension as the active circuit and run. For debugging convenience, I recommend going to the "Edit Scheme" and checking the "Debug executable" checkbox. This will simultaneously debug both the extension and the main application:


To install the keyboard, go to Settings -> General -> Keyboard -> Keyboards -> New keyboards ...


The screenshot on the right shows the dialog that appears when the user tries to allow full access to the keyboard. In truth, this text is hinting at me to choose "Don't allow."

As a bonus, I want to show the hierarchy of the view of the standard keyboard. I’ll leave it to myself, let everyone draw their own conclusions: The


full version of the source code is available on GitHub: Habrakeyboard

Demonstration




Publication


The publishing process, both the keyboard and other types of extensions, is practically the same as publishing a regular application, but the extension itself must satisfy some technical requirements:
  • An extension can only be published as part of a container application. Alone, it cannot exist. It is deleted when the application is uninstalled. By the way, this limitation is only for iOS. For Mac, the extension can be distributed separately;
  • Deployment Target for extension must be> = iOS 8.0. The main application can be published for earlier versions of the operating system. In older versions, the extension will simply not be available. That is, if, for example, the developers of the Habrahabr application want to implement something like this, then it will not be necessary to raise the Deployment Target;
  • Target for extension should contain a setting for assembly for processors with 64-bit architecture (arm64). If the application implements data exchange with the extension, then it must satisfy the same requirements. Say what you like, but you need to think about supporting 64-bit processors. For large projects with complex, low-level code, this can be very time consuming.

Apple also added several new items to the review guidelines. They are specific to keyboard extensions:
  • Having opened your keyboard, the user should be able to move on to the next;
  • The keyboard should remain operational if there is no Internet connection;
  • The keyboard should implement the following keyboard types: Number and Decimal (that is, if the input field implies entering a number, the keyboard should provide a convenient way for this);
  • The primary category for an application that contains a keyboard extension should be Utilities. Also, the application must provide the user with its own privacy policy;
  • The keyboard should use user data only to improve its functionality.

Original Recommendations
  • Keyboard extensions must provide a method for progressing to the next keyboard;
  • Keyboard extensions must remain functional with no network access or they will be rejected;
  • Keyboard extensions must provide Number and Decimal keyboard types as described in the App Extension Programming Guide or they will be rejected;
  • Apps offering Keyboard extensions must have a primary category of Utilities and a privacy policy or they will be rejected;
  • Apps offering Keyboard extensions may only collect user activity to enhance the functionality of their keyboard extension on the iOS device or they may be rejected.

If all the above requirements are met, then it remains to perform several basically known steps:
  1. Go to the developer portal and create an App ID for the extension. It should be a continuation of the identifier of the main application. For example, if the App ID for the application: com.company.application, the App ID for expansion can be: com.company.application.keyboard. Next, for the new App ID, you need to create a provisioning profile. This data must be specified in the Target settings in Xcode;
  2. If the extension implements data exchange with the main application, then in the developer's portal it is necessary to create the App Group, and also enable the ability to use the App Group in the App IDs settings for the extension and application. Actually, this step must be performed at the application testing stage, otherwise you will not see the data exchange;
  3. Add screenshots and a description for your extension in iTunesConnect. Apple has not added special fields for these purposes. These data are filled in along with the information on the main application;
  4. Collect the Target of the main application and send it to the review. The extension is already contained in the application package;
  5. That's all, we can only wait.

A guidance document on the application verification: the App Store Guidelines Review,

Also popular now: