15

我需要为我的 iPhone 应用程序构建一个自定义键盘。以前关于该主题的问题和答案都集中在自定义键盘的视觉元素上,但我试图了解如何从此键盘检索击键。

Apple 提供了 inputView 机制,可以轻松地将自定义键盘与 UITextField 或 UITextView 关联,但它们不提供将生成的击键发送回关联对象的功能。基于这些对象的典型委托,我们期望三个函数:一个普通字符,一个用于退格,一个用于回车。然而,似乎没有人明确定义这些功能或如何使用它们。

如何为我的 iOS 应用程序构建自定义键盘并从中检索击键?

4

4 回答 4

18

Greg 的方法应该有效,但我有一种方法不需要告诉键盘有关文本字段或文本视图的信息。事实上,您可以创建一个键盘实例并将其分配给多个文本字段和/或文本视图。键盘处理知道哪个是第一响应者。

这是我的方法。我不会展示任何用于创建键盘布局的代码。那是容易的部分。此代码显示所有管道。

编辑:这已更新为正确处理UITextFieldDelegate textField:shouldChangeCharactersInRange:replacementString:UITextViewDelegate textView:shouldChangeTextInRange:replacementText:.

头文件:

@interface SomeKeyboard : UIView <UIInputViewAudioFeedback>

@end

实现文件:

@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

此类需要 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
于 2012-11-03T02:48:00.160 回答
14

我已经为 iPad 创建了一个完整的工作示例,可以在 Github 上找到:

https://github.com/lnafziger/Numberpad

Numberpad 是 iPad 的自定义数字键盘,它可以与UITextField's 和' 一起使用,UITextView除了添加 Numberpad 类的实例作为文本字段/视图的 inputView 之外,不需要任何更改。

特征:

  • 它受 MIT 许可保护,因此可以根据其条款自由复制和使用。
  • 它适用于 UITextFields 和 UITextViews
  • 它不需要设置委托。
  • 它会自动跟踪哪个视图是第一响应者(因此您不必这样做)
  • 您不必设置键盘的大小或跟踪它。
  • 有一个共享实例,您可以将其用于任意数量的输入视图,而无需为每个视图使用额外的内存。

使用就像包含 Numberpad.h 一样简单,然后:

theTextField.inputView  = [Numberpad defaultNumberpad];

其他一切都会自动处理!

从 Github(上面的链接)中获取两个类文件和 xib,或者创建按钮(在代码中或在故事板/xib 中),并将其操作设置为类中的适当方法(numberpadNumberPressed、numberpadDeletePressed、numberpadClearPressed 或 numberpadDonePressed )。

以下代码已过时。最新代码见 Github 项目。

数字键盘.h:

#import <UIKit/UIKit.h>

@interface Numberpad : UIViewController

// The one and only Numberpad instance you should ever need:
+ (Numberpad *)defaultNumberpad;

@end

数字键盘.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
于 2012-11-12T21:15:56.260 回答
12

这是我的自定义键盘,我相信它可以在 Apple 允许的情况下完全解决这些问题:

//  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

在 XCode 4.3 及更高版本中,您需要创建一个基于 UIView 的目标类(用于 .h 和 .m 文件)和一个用户界面视图文件(用于 .xib 文件)。确保所有三个文件具有相同的名称。使用身份检查器,确保设置 XIB 的文件所有者自定义类以匹配新对象的名称。使用属性检查器,将表单的大小设置为自由格式并将状态栏设置为无。使用 Size Inspector,设置表单的大小,它应该与标准键盘的宽度相匹配(iPhone 纵向为 320,iPhone 横向为 480),但您可以选择任何您喜欢的高度。

表格可以使用了。添加按钮并将它们连接到 dataPress、backPress 和 enterPress(视情况而定)。initWithFrame: 和 loadWithNIB 函数将发挥所有魔力,让您可以使用在 Interface Builder 中设计的键盘。

要将此键盘与 UITextField myTextField 一起使用,只需将以下代码添加到您的 viewDidLoad:

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

由于某些限制,此键盘不可重复使用,因此您需要每个字段一个。我几乎可以让它可重复使用,但我只是觉得没那么聪明。键盘也仅限于 UITextFields,但这主要是因为实现回车键功能的限制,我将在下面解释。

这是让您设计出比此入门框架更好的键盘的魔力...

我使用谨慎的离散设置器 (setTextField) 实现了这个键盘的唯一属性 textField,因为:

  1. 我们需要 UITextField 对象来处理输入问题
  2. 我们需要 UITextField,因为它符合符合 UIKeyInput 的 UITextInput 协议,它完成了我们大部分繁重的工作
  3. 这是一个方便的地方,可以将 UITextInput 的 inputView 字段设置为使用此键盘。

您会注意到另一个名为 delegate 的私有属性,它本质上将 UITextField 指针类型转换为 UITextInput 指针。我可能可以内联完成这个转换,但我觉得这可能对未来的扩展很有用,可能包括对 UITextView 的支持。

函数 dataPress 是使用 UIKeyInput 的 insertText 方法在编辑字段中插入文本。这似乎适用于 iOS 4 的所有版本。对于我的键盘,我只是使用每个按钮的标签,这很正常。使用任何你喜欢的 NSStrings。

函数 dataBack 执行退格操作,并且稍微复杂一些。当 UIKeyInput deleteBackward 工作时,它工作得非常好。虽然文档说它可以回溯到 iOS 3.2,但它似乎只能回溯到 iOS 5.0,也就是 UITextField(和 UITextView)符合 UITextInput 协议的时候。所以在此之前,你是靠自己的。由于 iOS 4 支持是许多人关心的问题,我实现了一个蹩脚的退格键,它直接在 UITextField 上工作。如果不是因为这个要求,我可以让这个键盘与 UITextView 一起工作。而且这个退格不是一般的,只删除最后一个字符,而deleteBackward即使用户移动光标也能正常工作。

函数 enterPress 实现了 enter 键,但它是一个完整的组合,因为 Apple 似乎没有提供调用 enter 键的方法。因此 enterPress 只需调用 UITextField 的委托函数 textFieldShouldReturn:,这是大多数程序员实现的。请注意,这里的委托是 UITextField 的 UITextFieldDelegate 而不是键盘本身的委托属性。

这个解决方案绕过了正常的键盘处理,这在 UITextField 的情况下几乎不重要,但是使这种技术无法用于 UITextView,因为现在可以在正在编辑的文本中插入换行符。

差不多就是这样。完成这项工作需要 24 小时的阅读和整理。我希望它可以帮助某人。

于 2012-11-03T01:40:47.370 回答
0

(这主要取自http://blog.carbonfive.com/2012/03/12/customizing-the-ios-keyboard/

在 iOS 中,视图的键盘由UIResponder视图继承链的一部分管理。当任何需要键盘的 UIResponder 成为第一响应者(被录音或以其他方式激活)时,UIResponder 在其inputView属性中查找要显示为键盘的视图。因此,要制作自定义键盘并响应其上的事件,您必须创建一个带有字母按钮的视图,将视图控制器与该视图相关联,并与处理按下的按钮相关联,并且您必须将该视图设置为inputView一些文本框。

查看链接以获取更多信息。

于 2012-11-03T01:43:14.840 回答