How to get keystrokes from a custom keyboard in an iOS app?

I need to create a custom keyboard for my iPhone application. Previous questions and answers on the topic have focused on the visual elements of the user keyboard, but I'm trying to figure out how to extract keystrokes from this keyboard.

Apple provides an inputView mechanism that makes it easy to associate a custom keyboard with a UITextField or UITextView, but they do not provide functions for sending generated keystrokes to a related object. Based on typical delegation for these objects, we expect three functions: one of normal characters, one for backspace and one for input. However, no one seems to clearly define these features or how to use them.

How to create a custom keyboard for my iOS application and get keystrokes from it?

+14
ios keyboard
Nov 03
source share
4 answers

Here's my custom keyboard, which I believe is addressed to them completely, like Apple, will allow:

// PVKeyboard.h #import <UIKit/UIKit.h> @interface PVKeyboard : UIView @property (nonatomic,assign) UITextField *textField; @end // PVKeyboard.m #import "PVKeyboard.h" @interface PVKeyboard () { UITextField *_textField; } @property (nonatomic,assign) id<UITextInput> delegate; @end @implementation PVKeyboard - (id<UITextInput>) delegate { return _textField; } - (UITextField *)textField { return _textField; } - (void)setTextField:(UITextField *)tf { _textField = tf; _textField.inputView = self; } - (IBAction)dataPress:(UIButton *)btn { [self.delegate insertText:btn.titleLabel.text]; } - (IBAction)backPress { if ([self.delegate conformsToProtocol:@protocol(UITextInput)]) { [self.delegate deleteBackward]; } else { int nLen = [_textField.text length]; if (nLen) _textField.text = [_textField.text substringToIndex:nLen-1]; } } - (IBAction)enterPress { [_textField.delegate textFieldShouldReturn:_textField]; } - (UIView *)loadWithNIB { NSArray *aNib = [[NSBundle mainBundle]loadNibNamed:NSStringFromClass([self class]) owner:self options:nil]; UIView *view = [aNib objectAtIndex:0]; [self addSubview:view]; return view; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) [self loadWithNIB]; return self; } @end 

In Xcode 4.3 and later, you need to create an object class (for .h and .m files) based on a UIView file and a user interface file (for a .xib file). Make sure all three files have the same name. Using the Identity Inspector, verify that the user class of the owner of the XIB file matches the name of the new object. Using the Attributes Inspector, set the size of the form in Freeform and set the status bar to none. Using the Size Inspector, set the size of the form to fit the width of the standard keyboard (320 for iPhone and 480 for iPhone), but you can choose any height you need.

The form is ready to use. Add buttons and connect them to dataPress, backPress and enterPress, if necessary. The initWithFrame: and loadWithNIB functions will do all the magic so you can use the keyboard developed in Interface Builder.

To use this keyboard with a UITextField myTextField, simply add the following code to your viewDidLoad:

 self.keyboard = [[PVKeyboard alloc]initWithFrame:CGRectMake(0,488,320,60)]; self.keyboard.textField = self.myTextField; 

Due to some restrictions, this keyboard cannot be reused, so you will need one field. I can almost make it reusable, but I just don't feel this smart. The keyboard is also limited by UITextFields, but mainly due to limitations in the implementation of function input keys, which I will explain below.

Here is the magic that should allow you to create a better keyboard than this starting frame ...

I implemented the only property of this textField keyboard using a discrete discrete setter (setTextField), because:

  • we need a UITextField object to handle input problems
  • we need a UITextField because it conforms to the UITextInput protocol, which conforms to UIKeyInput, which does most of our heavy lifting
  • It was a convenient place to set the Input UITextInput input field to use this keyboard.

You will see a second private property called delegate, which essentially outputs a UITextField pointer to a UITextInput pointer. Maybe I could do this, but I realized that it might be useful as a function for future extensions, perhaps to include support for UITextView.

The dataPress function is that it inserts text, enters an edited field using the insertText method for UIKeyInput. This seems to work in all versions for iOS 4. For my keyboard, I just use the label of each button, which is pretty normal. Use all that NSStrings amazes with your imagination.

The dataBack function does inverse space and is a bit more complicated. When UIKeyInput deleteBackward works, it works wonderfully. And although the documentation says that it works with iOS 3.2, it looks like it only works with iOS 5.0 when the UITextField (and UITextView) conforms to the UITextInput protocol. Therefore, before that, you yourself. Since iOS 4 support is a problem for many, I have implemented a lame backspace that works directly with UITextField. If not for this requirement, I could make this keyboard work with UITextView. And this backspace is not common, it only deletes the last character, and deleteBackward will work correctly even if the user moves the cursor.

The enterPress function implements an input key, but is a complete kludge because Apple does not seem to provide a method for invoking an input key. Thus, enterPress simply calls the UITextField textFieldShouldReturn: delegate function, which most programmers implement. Note that the delegate here is UITextFieldDelegate for UITextField and NOT a delegate property for the keyboard itself.

This solution extends to normal keyboard processing, which is hardly relevant in the case of UITextField, but makes this technique unsuitable for use with UITextView, as there is now a way to insert line breaks in editable text.

This is pretty much the case. This work required 24 hours of reading and paving. Hope this helps someone.

+12
Nov 03 '12 at 1:40
source share

Greg's approach should work, but I have an approach that does not require the keyboard to be told about a text box or text view. In fact, you can create one instance of the keyboard and assign it to multiple text fields and / or text views. The keyboard processes information about which one is the first responder.

Here is my approach. I am not going to show code for creating a keyboard layout. This is the easy part. This code shows all the plumbing.

Change This update has been updated to properly handle UITextFieldDelegate textField:shouldChangeCharactersInRange:replacementString: and UITextViewDelegate textView:shouldChangeTextInRange:replacementText:

Header file:

 @interface SomeKeyboard : UIView <UIInputViewAudioFeedback> @end 

Implementation File:

 @implmentation SomeKeyboard { id<UITextInput> _input; BOOL _tfShouldChange; BOOL _tvShouldChange; } - (id)init { self = [super init]; if (self) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkInput:) name:UITextFieldTextDidBeginEditingNotification object:nil]; } return self; } // This is used to obtain the current text field/view that is now the first responder - (void)checkInput:(NSNotification *)notification { UITextField *field = notification.object; if (field.inputView && self == field.inputView) { _input = field; _tvShouldChange = NO; _tfShouldChange = NO; if ([_input isKindOfClass:[UITextField class]]) { id<UITextFieldDelegate> del = [(UITextField *)_input delegate]; if ([del respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { _tfShouldChange = YES; } } else if ([_input isKindOfClass:[UITextView class]]) { id<UITextViewDelegate> del = [(UITextView *)_input delegate]; if ([del respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { _tvShouldChange = YES; } } } } // Call this for each button press - (void)click { [[UIDevice currentDevice] playInputClick]; } // Call this when a button on the keyboard is tapped (other than return or backspace) - (void)keyTapped:(UIButton *)button { NSString *text = ???; // determine text for the button that was tapped if ([_input respondsToSelector:@selector(shouldChangeTextInRange:replacementText:)]) { if ([_input shouldChangeTextInRange:[_input selectedTextRange] replacementText:text]) { [_input insertText:text]; } } else if (_tfShouldChange) { NSRange range = [(UITextField *)_input selectedRange]; if ([[(UITextField *)_input delegate] textField:(UITextField *)_input shouldChangeCharactersInRange:range replacementString:text]) { [_input insertText:text]; } } else if (_tvShouldChange) { NSRange range = [(UITextView *)_input selectedRange]; if ([[(UITextView *)_input delegate] textView:(UITextView *)_input shouldChangeTextInRange:range replacementText:text]) { [_input insertText:text]; } } else { [_input insertText:text]; } } // Used for a UITextField to handle the return key button - (void)returnTapped:(UIButton *)button { if ([_input isKindOfClass:[UITextField class]]) { id<UITextFieldDelegate> del = [(UITextField *)_input delegate]; if ([del respondsToSelector:@selector(textFieldShouldReturn:)]) { [del textFieldShouldReturn:(UITextField *)_input]; } } else if ([_input isKindOfClass:[UITextView class]]) { [_input insertText:@"\n"]; } } // Call this to dismiss the keyboard - (void)dismissTapped:(UIButton *)button { [(UIResponder *)_input resignFirstResponder]; } // Call this for a delete/backspace key - (void)backspaceTapped:(UIButton *)button { if ([_input respondsToSelector:@selector(shouldChangeTextInRange:replacementText:)]) { UITextRange *range = [_input selectedTextRange]; if ([range.start isEqual:range.end]) { UITextPosition *newStart = [_input positionFromPosition:range.start inDirection:UITextLayoutDirectionLeft offset:1]; range = [_input textRangeFromPosition:newStart toPosition:range.end]; } if ([_input shouldChangeTextInRange:range replacementText:@""]) { [_input deleteBackward]; } } else if (_tfShouldChange) { NSRange range = [(UITextField *)_input selectedRange]; if (range.length == 0) { if (range.location > 0) { range.location--; range.length = 1; } } if ([[(UITextField *)_input delegate] textField:(UITextField *)_input shouldChangeCharactersInRange:range replacementString:@""]) { [_input deleteBackward]; } } else if (_tvShouldChange) { NSRange range = [(UITextView *)_input selectedRange]; if (range.length == 0) { if (range.location > 0) { range.location--; range.length = 1; } } if ([[(UITextView *)_input delegate] textView:(UITextView *)_input shouldChangeTextInRange:range replacementText:@""]) { [_input deleteBackward]; } } else { [_input deleteBackward]; } [self updateShift]; } @end 

This class requires a category method for UITextField:

 @interface UITextField (CustomKeyboard) - (NSRange)selectedRange; @end @implementation UITextField (CustomKeyboard) - (NSRange)selectedRange { UITextRange *tr = [self selectedTextRange]; NSInteger spos = [self offsetFromPosition:self.beginningOfDocument toPosition:tr.start]; NSInteger epos = [self offsetFromPosition:self.beginningOfDocument toPosition:tr.end]; return NSMakeRange(spos, epos - spos); } @end 
+18
Nov 03
source share

I created a full working keyboard example for the iPad, available on Github here:

https://github.com/lnafziger/Numberpad

Numpad is a regular numeric keypad for iPad, which works with both UITextField and UITextView , which do not require any changes, except for adding an instance of the Numpad class as the input text for the Field / View text.

Features:

  • It is distributed under the MIT license, so it can be freely copied and used on its own "terms."
  • It works with UITextFields and UITextViews
  • This does not require the installation of a delegate.
  • It automatically keeps track of which view is the first responder (so you don't need to)
  • You do not need to set the keyboard size or track it.
  • There is a generic instance that you can use for as many input representations as you want, without using additional memory for each of them.

Using is as simple as Numpad.h, and then:

 theTextField.inputView = [Numberpad defaultNumberpad]; 

Everything else takes care automatically!

Either take two class files and xib from Github (link above), or create buttons (in code or in the / xib storyboard) with their actions set for the corresponding methods in the class (numberpadNumberPressed, numberpadDeletePressed, numberpadClearPressed, or numberpadDonePressed).

The following code is deprecated. See the Github Project for the latest code.

Numberpad.h:

 #import <UIKit/UIKit.h> @interface Numberpad : UIViewController // The one and only Numberpad instance you should ever need: + (Numberpad *)defaultNumberpad; @end 

Numberpad.m:

 #import "Numberpad.h" #pragma mark - Private methods @interface Numberpad () @property (nonatomic, weak) id<UITextInput> targetTextInput; @end #pragma mark - Numberpad Implementation @implementation Numberpad @synthesize targetTextInput; #pragma mark - Shared Numberpad method + (Numberpad *)defaultNumberpad { static Numberpad *defaultNumberpad = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ defaultNumberpad = [[Numberpad alloc] init]; }); return defaultNumberpad; } #pragma mark - view lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Keep track of the textView/Field that we are editing [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(editingDidBegin:) name:UITextFieldTextDidBeginEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(editingDidBegin:) name:UITextViewTextDidBeginEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(editingDidEnd:) name:UITextFieldTextDidEndEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(editingDidEnd:) name:UITextViewTextDidEndEditingNotification object:nil]; } - (void)viewDidUnload { [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidEndEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil]; self.targetTextInput = nil; [super viewDidUnload]; } #pragma mark - editingDidBegin/End // Editing just began, store a reference to the object that just became the firstResponder - (void)editingDidBegin:(NSNotification *)notification { if (![notification.object conformsToProtocol:@protocol(UITextInput)]) { self.targetTextInput = nil; return; } self.targetTextInput = notification.object; } // Editing just ended. - (void)editingDidEnd:(NSNotification *)notification { self.targetTextInput = nil; } #pragma mark - Keypad IBActions // A number (0-9) was just pressed on the number pad // Note that this would work just as well with letters or any other character and is not limited to numbers. - (IBAction)numberpadNumberPressed:(UIButton *)sender { if (!self.targetTextInput) { return; } NSString *numberPressed = sender.titleLabel.text; if ([numberPressed length] == 0) { return; } UITextRange *selectedTextRange = self.targetTextInput.selectedTextRange; if (!selectedTextRange) { return; } [self textInput:self.targetTextInput replaceTextAtTextRange:selectedTextRange withString:numberPressed]; } // The delete button was just pressed on the number pad - (IBAction)numberpadDeletePressed:(UIButton *)sender { if (!self.targetTextInput) { return; } UITextRange *selectedTextRange = self.targetTextInput.selectedTextRange; if (!selectedTextRange) { return; } // Calculate the selected text to delete UITextPosition *startPosition = [self.targetTextInput positionFromPosition:selectedTextRange.start offset:-1]; if (!startPosition) { return; } UITextPosition *endPosition = selectedTextRange.end; if (!endPosition) { return; } UITextRange *rangeToDelete = [self.targetTextInput textRangeFromPosition:startPosition toPosition:endPosition]; [self textInput:self.targetTextInput replaceTextAtTextRange:rangeToDelete withString:@""]; } // The clear button was just pressed on the number pad - (IBAction)numberpadClearPressed:(UIButton *)sender { if (!self.targetTextInput) { return; } UITextRange *allTextRange = [self.targetTextInput textRangeFromPosition:self.targetTextInput.beginningOfDocument toPosition:self.targetTextInput.endOfDocument]; [self textInput:self.targetTextInput replaceTextAtTextRange:allTextRange withString:@""]; } // The done button was just pressed on the number pad - (IBAction)numberpadDonePressed:(UIButton *)sender { if (!self.targetTextInput) { return; } // Call the delegate methods and resign the first responder if appropriate if ([self.targetTextInput isKindOfClass:[UITextView class]]) { UITextView *textView = (UITextView *)self.targetTextInput; if ([textView.delegate respondsToSelector:@selector(textViewShouldEndEditing:)]) { if ([textView.delegate textViewShouldEndEditing:textView]) { [textView resignFirstResponder]; } } } else if ([self.targetTextInput isKindOfClass:[UITextField class]]) { UITextField *textField = (UITextField *)self.targetTextInput; if ([textField.delegate respondsToSelector:@selector(textFieldShouldEndEditing:)]) { if ([textField.delegate textFieldShouldEndEditing:textField]) { [textField resignFirstResponder]; } } } } #pragma mark - text replacement routines // Check delegate methods to see if we should change the characters in range - (BOOL)textInput:(id <UITextInput>)textInput shouldChangeCharactersInRange:(NSRange)range withString:(NSString *)string { if (!textInput) { return NO; } if ([textInput isKindOfClass:[UITextField class]]) { UITextField *textField = (UITextField *)textInput; if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { if (![textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string]) { return NO; } } } else if ([textInput isKindOfClass:[UITextView class]]) { UITextView *textView = (UITextView *)textInput; if ([textView.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { if (![textView.delegate textView:textView shouldChangeTextInRange:range replacementText:string]) { return NO; } } } return YES; } // Replace the text of the textInput in textRange with string if the delegate approves - (void)textInput:(id <UITextInput>)textInput replaceTextAtTextRange:(UITextRange *)textRange withString:(NSString *)string { if (!textInput) { return; } if (!textRange) { return; } // Calculate the NSRange for the textInput text in the UITextRange textRange: int startPos = [textInput offsetFromPosition:textInput.beginningOfDocument toPosition:textRange.start]; int length = [textInput offsetFromPosition:textRange.start toPosition:textRange.end]; NSRange selectedRange = NSMakeRange(startPos, length); if ([self textInput:textInput shouldChangeCharactersInRange:selectedRange withString:string]) { // Make the replacement: [textInput replaceRange:textRange withText:string]; } } @end 
+14
Nov 12
source share

(Basically this is taken from http://blog.carbonfive.com/2012/03/12/customizing-the-ios-keyboard/ )

On iOS, the keyboard for presentation is controlled by part of the UIResponder view inheritance chain. When any UIResponder that needs a keyboard becomes the first responder (taped or otherwise activated), the UIResponder looks in the inputView property to display as a keyboard. So, in order to create a custom keyboard and respond to an event, you need to create a view using the letter buttons, associate the view controller with this view and buttons for processing presses, and you must set this view as inputView some text field.

Take a look at the link for more information.

0
Nov 03
source share



All Articles