iOS Keyboard Delete Key Response#
Background#
The background is to implement a feature where, when the delete key on the keyboard is pressed, the selected objects in a multiple selection with an input box are deleted.
Implementation#
Since UITextField
does not have a delegate for the delete key, my initial idea was to use textField:shouldChangeCharactersInRange:replacementString:
to implement the listener. When the current string is empty and the replacement string is also empty, it means that the delete button was pressed. In this case, I would use a block method to handle the event. The code is as follows:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
if ((textField.text.length == 0) && (string.length == 0)) {
if (self.deleteBackwardBlock) {
self.deleteBackwardBlock
}
}
return YES;
}
After testing, I found that this logic works fine with third-party input methods, but with the native system input method, when the textField is empty, the delegate method is not called, so this method does not work.
Then, I looked it up and found that it is possible to use runtime to get the deleteBackward
event. By hooking into this event, I can capture the event when the delete button on the keyboard is pressed. The code is as follows:
// UITextField+BackSpace.h
#import <UIKit/UIKit.h>
@protocol BackSpaceDelegate <NSObject>
@optional
- (void)textFieldBackSpaceTapped:(UITextField *)textField;
@end
@interface UITextField (BackSpace)
@property (nonatomic, weak) id<BackSpaceDelegate>bsDelegate;
@property (nonatomic, copy) void(^ backSpaceCallback)(void);
@end
// UITextField+BackSpace.m
#import "UITextField+BackSpace.h"
#import <objc/runtime.h>
@implementation UITextField (BackSpace)
static const char *kDelegatePropertyKey = "kDelegatePropertyKey";
static const char *kBlockPropertyKey = "kBlockPropertyKey";
+ (void)load {
Method originalMethod = class_getInstanceMethod([self class], NSSelectorFromString(@"deleteBackward"));
Method targetMethod = class_getInstanceMethod([self class], @selector(mk_deleteBackward));
method_exchangeImplementations(originalMethod, targetMethod);
}
- (id<BackSpaceDelegate>)bsDelegate {
return objc_getAssociatedObject(self, kDelegatePropertyKey);
}
- (void)setBsDelegate:(id<BackSpaceDelegate>)bsDelegate {
objc_setAssociatedObject(self, kDelegatePropertyKey, bsDelegate, OBJC_ASSOCIATION_ASSIGN);
}
- (void (^)(void))backSpaceCallback {
return objc_getAssociatedObject(self, kBlockPropertyKey);
}
- (void)setBackSpaceCallback:(void (^)(void))backSpaceCallback {
objc_setAssociatedObject(self, kBlockPropertyKey, backSpaceCallback, OBJC_ASSOCIATION_COPY);
}
- (void)mk_deleteBackward {
[self mk_deleteBackward];
if ([self.bsDelegate respondsToSelector:@selector(textFieldBackSpaceTapped:)]) {
[self.bsDelegate textFieldBackSpaceTapped:self];
}
}
Then, in the place where it is used, set textField.bsDelegate
and implement the textFieldBackSpaceTapped:
method. After testing, I found that the delegate method does respond when the delete key on the keyboard is pressed. The code is as follows:
@interface TargetView ()<BackSpaceDelegate>
@property (nonatomic, strong) UITextField *textField;
@end
@implementation TargetView
...
self.textField.delegate = self;
self.textField.bsDelegate = self;
...
- (void)textFieldBackSpaceTapped:(UITextField *)textField {
NSLog(@"Delete");
}
@end
Looking back at the requirement, when there is no data in the input box, the selected results in the multiple selection should be deleted. So I directly added a check in this delegate method. If the textField's text is not empty, I update the previous string and return. If the textField's text is empty and the previous string is not empty, I update the previous string and return. If both the textField's text and the previous string are empty, I delete the selected result from the multiple selection.
The code is as follows:
- (void)textFieldBackSpaceTapped:(UITextField *)textField {
NSLog(@"Delete");
if (textField.text.length != 0) {
self.previousStr = textField.text;
return;
}
if (self.previousStr.length != 0) {
self.previousStr = textField.text;
return;
}
UIView *lastView = self.multipleSelectView.subviews.lastObject;
if (lastView) {
[lastView removeFromSuperview];
}
}
After debugging, I found that when the last character is reached and the delete button is clicked, both the character and the multiple selection are deleted together. However, what we want is to perform the multiple selection operation only after the last character is deleted and the delete button is clicked again.
My initial understanding was that the delete button event comes first, and when the delete button is clicked, the text obtained from the textField should be the text before deletion, and then the textField:shouldChangeCharactersInRange:replacementString:
method should be called. However, after debugging, I found that the actual order is that the delete button is clicked, then textField:shouldChangeCharactersInRange:replacementString:
is executed, and finally textFieldBackSpaceTapped:
is called.
So the above situation occurs. How can we solve it?
The simplest solution is to keep track of the previous value of the input box. When the previous value is empty, only then can the multiple selection data be deleted. Otherwise, the multiple selection data is not modified, and only the previous value of the input box is updated.
The code is as follows:
- (void)textFieldBackSpaceTapped:(UITextField *)textField {
NSLog(@"Delete");
if (textField.text.length != 0) {
self.previousStr = textField.text;
return;
}
if (self.previousStr.length != 0) {
self.previousStr = textField.text;
return;
}
UIView *lastView = self.multipleSelectView.subviews.lastObject;
if (lastView) {
[lastView removeFromSuperview];
}
}
The effect is as follows:
Code reference:
BackSpace