15

iPhone アプリ用のカスタム キーボードを作成する必要があります。このトピックに関する以前の質問と回答は、カスタム キーボードの視覚要素に焦点を当てていましたが、このキーボードからキーストロークを取得する方法を理解しようとしています。

Apple は、カスタム キーボードを UITextField または UITextView に簡単に関連付けることができる inputView メカニズムを提供していますが、生成されたキーストロークを関連付けられたオブジェクトに送り返す機能は提供していません。これらのオブジェクトの典型的な委任に基づいて、通常の文字の 1 つ、バックスペース用の 1 つ、Enter 用の 1 つの 3 つの関数が期待されます。しかし、これらの機能や使用方法を明確に定義している人は誰もいないようです。

iOS アプリ用のカスタム キーボードを作成し、そこからキーストロークを取得するにはどうすればよいですか?

4

4 に答える 4

18

グレッグのアプローチは機能するはずですが、テキスト フィールドまたはテキスト ビューについてキーボードに通知する必要のないアプローチがあります。実際、キーボードのインスタンスを 1 つ作成して、それを複数のテキスト フィールドやテキスト ビューに割り当てることができます。キーボードは、どちらがファーストレスポンダであるかを認識して処理します。

これが私のアプローチです。キーボード レイアウトを作成するためのコードは示しません。それは簡単な部分です。このコードは、すべての配管を示しています。

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 は、テキスト フィールド/ビューの inputView として Numberpad クラスのインスタンスを追加する以外は、UITextFieldとの両方で動作する iPad 用のカスタム数値キーボードです。UITextView

特徴:

  • これは MIT ライセンスの下でカバーされているため、その条件に従って自由にコピーして使用することができます。
  • UITextFields と UITextViews で動作します
  • デリゲートを設定する必要はありません。
  • どのビューが最初の応答者であるかを自動的に追跡します (その必要はありません)。
  • キーボードのサイズを設定したり、追跡したりする必要はありません。
  • 入力ビューごとに余分なメモリを使用することなく、好きなだけ入力ビューに使用できる共有インスタンスがあります。

使用法は Numberpad.h をインクルードするのと同じくらい簡単です。

theTextField.inputView  = [Numberpad defaultNumberpad];

他のすべては自動的に処理されます。

Github (上のリンク) から 2 つのクラス ファイルと xib を取得するか、クラス内の適切なメソッド (numberpadNumberPressed、numberpadDeletePressed、numberpadClearPressed、または numberpadDonePressed) に設定されたアクションを使用して (コード内またはストーリーボード/xib 内に) ボタンを作成します。 )。

次のコードは古くなっています。最新のコードについては、Github プロジェクトを参照してください。

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
于 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 とユーザー インターフェイス ビュー ファイル (.xib ファイル用) に基づいて、objective-Class (.h & .m ファイル用) を作成する必要があります。3 つのファイルがすべて同じ名前であることを確認します。Identity Inspector を使用して、XIB の File's Owner Custom Class が新しいオブジェクトの名前と一致するように設定してください。属性インスペクタを使用して、フォームのサイズをフリーフォームに設定し、ステータス バーをなしに設定します。Size Inspector を使用して、フォームのサイズを標準キーボードの幅 (iPhone の縦向きの場合は 320、横向きの場合は 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;

いくつかの制限により、このキーボードは再利用できないため、フィールドごとに 1 つ必要になります。ほぼ再利用可能にできますが、それほど賢くはありません。キーボードも UITextFields に制限されていますが、これは主に、以下で説明するエンター キー機能の実装の制限によるものです。

このスターター フレームワークよりも優れたキーボードを設計できる魔法を次に示します...

このキーボードの唯一のプロパティである textField を、個別のセッター (setTextField) を使用して実装しました。

  1. 入力の問題を処理するには UITextField オブジェクトが必要です
  2. UITextField が必要なのは、UIKeyInput に準拠する UITextInput プロトコルに準拠しているためです。
  3. このキーボードを使用するように UITextInput の inputView フィールドを設定するのに便利な場所でした。

デリゲートという名前の 2 番目のプライベート プロパティに気付くでしょう。これは、本質的に UITextField ポインターを UITextInput ポインターに型キャストします。このキャストをインラインで行うこともできたかもしれませんが、おそらく UITextView のサポートを含めるために、これが将来の拡張機能として役立つ可能性があると感じました。

関数 dataPress は、UIKeyInput の insertText メソッドを使用して編集フィールドにテキスト入力を挿入するものです。これは、iOS 4 までさかのぼるすべてのバージョンで機能するようです。私のキーボードでは、各ボタンのラベルを単純に使用していますが、これはごく普通のことです。好きな NSString を使用してください。

関数 dataBack はバックスペースを行い、もう少し複雑です。UIKeyInput の deleteBackward が機能すると、見事に機能します。また、ドキュメントには iOS 3.2 まで動作すると書かれていますが、UITextField (および UITextView) が UITextInput プロトコルに準拠していた iOS 5.0 までしか動作しないようです。その前に、あなたは一人でいます。iOS 4 のサポートは多くの人にとって懸念事項であるため、UITextField で直接動作する不十分なバックスペースを実装しました。この要件がなければ、このキーボードを UITextView で動作させることができたはずです。また、このバックスペースは一般的ではなく、最後の文字を削除するだけですが、deleteBackward はユーザーがカーソルを移動しても適切に機能します。

関数 enterPress はエンター キーを実装しますが、Apple はエンター キーを呼び出す方法を提供していないように見えるため、完全なクラッジです。したがって、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 に答える