Стандартная библиотека предоставляет для UITextField
и UITextView
несколько различных типов клавиатур, среди которых есть обычная, Email, URL (от обычной отличаются несколькими символами) и Phone (цифровая). Для большинства жизненных ситуаций этого достаточно, но не всегда.
Представим, что в приложении есть поле ввода, могущее принимать числа и арифметические выражения.
Phone клавиатуры нам будет недостаточно — нет точки, нет всех символов операций, и т.д.
Безусловно, все необходимое есть на обычной клавиатуре, однако в данном случае 95% ее использоваться не будет (напомним, нам нужны только цифры + символы арифметических операций).
Вывод напрашивается сам собой: нужно писать свою клавиатуру, ну вы знаете с чем!
Итак, какие символы нам нужны? ".0123456789%+-×÷", и для коллекции добавим "↵" (return) и "←" (backspace). Нарисуем макет:
ОК, выглядит вроде неплохо. Теперь нужно прикрутить сие поделие к полю текстового ввода.
Еще со времен iOS 3 у компонентов интерфейса, позволяющих ввод (таких, как UITextField
и UITextView
) есть чудесные свойства inputView
и inputAccessoryView
.
Вьюха, лежащая в inputAccessoryView
, показывается над клавиатурой (само собой, когда последняя находится на экране). Обычно это тулбар с парой кнопок и/или полем для ввода.
А вот inputView
предназначено как раз для переопределения стандартной клавиатуры. Иными словами, если присвоить объект класса UIView
данному свойству, то в момент, когда текстовое поле становится firstResponder
, показывается не клавиатура, а тот самый объект UIView
, лежащий в inputView
. Показывается он в том же месте (снизу, хехе) и с теми же анимациями, что и обычная клавиатура. Более того, будут посылаться и уведомления вроде UIKeyboardWillShowNotification
! Сплошные плюсы.
А как же минусы? Их есть у меня.
Главный минус любого нестандартного ввода — это как организовать связь между клавиатурой и текстовым полем. Решений можно придумать немало, но, безусловно, идеальным было бы сделать свой ввод «нативным», максимально прозрачным для использования (например, какое-нибудь шаманство с протоколом UITextInputTraits
и расширение UIKeyboardType
). К сожалению, над этим я особо не задумывался, хотя, когда будет время, обязательно попробую допилить.
Одно из наиболее очевидных решений — делегат. Наша клавиатура будет просто сообщать своему делегату, какая кнопка нажата. И кстати о кнопках. А почему бы не хардкодить клавиатуру, а сделать ее динамически конфигурируемой? Действительно, почему бы и нет? Заведем для этого протокол Datasource.
Итак, имеем в итоге один класс и два протокола: клавиатура, delegate, datasource.
//KHKeyboard.h
#import <UIKit/UIKit.h>
#import "KHKeyboardDatasource.h"
#import "KHKeyboardDelegate.h"
@interface KHKeyboard : UIView
@property (nonatomic, assign) id<KHKeyboardDatasource> datasource;
@property (nonatomic, assign) id<KHKeyboardDelegate> delegate;
@end
//KHKeyboardDelegate.h
#import <UIKit/UIKit.h>
@class KHKeyboard;
@protocol KHKeyboardDelegate <NSObject>
@optional
- (void)keyPressedInKeyboard:(KHKeyboard *)keyboard atIndex:(NSInteger)index;
@end
//KHKeyboardDatasource.h
#import <UIKit/UIKit.h>
@class KHKeyboard;
@protocol KHKeyboardDatasource <NSObject>
@required
- (NSInteger)numberOfKeysInKeyboard:(KHKeyboard *)keyboard;
- (UIButton *)buttonForKeyInKeyboard:(KHKeyboard *)keyboard atIndex:(NSInteger)index;
@end
С делегатом все понятно — нажатая кнопка определяется по индексу. Datasource предназначен для конфигурирования нашей клавиатуры: он сообщает количество кнопок, а также предоставляет объекты UIButton
(собственно кнопки). Конфигурируя возвращаемую кнопку, не забываем установить свойство frame
— оно определяет положение кнопки на нашей вьюхе.
Инициализация:
self.rects = [NSArray arrayWithObjects:
[NSValue valueWithCGRect:CGRectMake(0, 0, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(64, 0, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(128, 0, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(192, 0, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(256, 0, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(0, 50, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(64, 50, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(128, 50, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(192, 50, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(256, 50, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(0, 100, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(64, 100, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(128, 100, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(192, 100, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(256, 100, 64, 100)],
[NSValue valueWithCGRect:CGRectMake(0, 150, 128, 50)],
[NSValue valueWithCGRect:CGRectMake(128, 150, 64, 50)],
[NSValue valueWithCGRect:CGRectMake(192, 150, 64, 50)],
nil];
self.titles = [NSArray arrayWithObjects:
@"7", @"8", @"9", @"×", @"←",
@"4", @"5", @"6", @"÷", @"%",
@"1", @"2", @"3", @"+", @"↵",
@"0", @".", @"−",
nil];
KHKeyboard *keyboard = [[[KHKeyboard alloc] init] autorelease];
keyboard.datasource = self;
keyboard.delegate = self;
self.textField.inputView = keyboard;
Настраиваем datasource:
- (NSInteger)numberOfKeysInKeyboard:(KHKeyboard *)keyboard
{
return [self.rects count];
}
- (UIButton *)buttonForKeyInKeyboard:(KHKeyboard *)keyboard atIndex:(NSInteger)index
{
NSString *title = [self.titles objectAtIndex:index];
CGRect rect = [(NSValue *)[self.rects objectAtIndex:index] CGRectValue];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = rect;
[button setTitle:title forState:UIControlStateNormal];
[button setUserInteractionEnabled:NO];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateHighlighted];
[button setReversesTitleShadowWhenHighlighted:YES];
if (index == 14) { // return key
[button setBackgroundImage:[UIImage imageNamed:@"button_normal_2.png"]
forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:@"button_highlighted_2.png"]
forState:UIControlStateHighlighted];
} else if (index == 15) { // "0" key
[button setBackgroundImage:[UIImage imageNamed:@"button_normal_1.png"]
forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:@"button_highlighted_1.png"]
forState:UIControlStateHighlighted];
} else {
[button setBackgroundImage:[UIImage imageNamed:@"button_normal.png"]
forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:@"button_highlighted.png"]
forState:UIControlStateHighlighted];
}
[button.titleLabel setFont:[UIFont boldSystemFontOfSize:16]];
return button;
}
И сиротливый delegate:
- (void)keyPressedInKeyboard:(KHKeyboard *)keyboard atIndex:(NSInteger)index
{
if (index == 14) { // return key
[self.textField resignFirstResponder];
} else if (index == 4) { // backspace key
NSInteger length = [self.textField.text length];
if (length == 0) {
return;
} else {
NSString *newValue = [self.textField.text substringToIndex:length - 1];
self.textField.text = newValue;
}
} else {
NSString *value = [self.titles objectAtIndex:index];
NSMutableString *newValue = [NSMutableString stringWithFormat:@"%@%@", self.textField.text, value];
self.textField.text = newValue;
}
}
Результат можно видеть на скриншоте в начале статьи, а также пощупать на github.
На этом, в принципе, можно было бы и завершить, однако остаются некоторые вопросы.
Например, данная реализация хорошо подходит, если поле ввода, требующее кастомную клавиатуру, только одно. А если их несколько? Нужно ведь как-то определять, какое из них является firstResponder
в текущий момент.
Как вариант, можно следить за уже упомянутыми уведомлениями о появлении клавиатуры (не забываем, что они никуда не исчезают). Свойство object
объекта класса NSNotification
будет содержать контрол, который вызвал клавиатуру. Следовательно, сохраняем его и используем впоследствии в keyPressedInKeyboard:atIndex:
Буду очень благодарен за любые дельные пожелания и подсказки по улучшению и унификации сего поделия.
Автор: khour