Пишем клавиатуру для нестандартного ввода под iOS

в 11:23, , рубрики: iOS, iphone, objective-c, разработка под iOS, метки: , ,

Пишем клавиатуру для нестандартного ввода под iOSСтандартная библиотека предоставляет для UITextField и UITextView несколько различных типов клавиатур, среди которых есть обычная, Email, URL (от обычной отличаются несколькими символами) и Phone (цифровая). Для большинства жизненных ситуаций этого достаточно, но не всегда.

Представим, что в приложении есть поле ввода, могущее принимать числа и арифметические выражения.

Phone клавиатуры нам будет недостаточно — нет точки, нет всех символов операций, и т.д.

Безусловно, все необходимое есть на обычной клавиатуре, однако в данном случае 95% ее использоваться не будет (напомним, нам нужны только цифры + символы арифметических операций).

Вывод напрашивается сам собой: нужно писать свою клавиатуру, ну вы знаете с чем!

Итак, какие символы нам нужны? ".0123456789%+-×÷", и для коллекции добавим "↵" (return) и "←" (backspace). Нарисуем макет:

Пишем клавиатуру для нестандартного ввода под iOS

ОК, выглядит вроде неплохо. Теперь нужно прикрутить сие поделие к полю текстового ввода.

Еще со времен 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js