Отключение клавиатуры при касании в любом месте UITextField

Я разрабатываю приложение для iPad, которое имеет большое количество UIViewControllers, UITableViews (с ячейками с accessoryViews для UITextFields) и т.д. И т.д. Многие из UIViewControllers появляются в иерархии навигации.

Есть много разных мест, где появляются UITextFields, в том числе как UITableViewCell accessoryViews.

Я хотел бы разработать эффективную стратегию для отклонения клавиатуры всякий раз, когда пользователь касается вне UITextField, редактируемого в настоящее время. Я искал методы отклонения клавиатуры, но пока не нашел ответа, объясняющего, как может работать общая стратегия отклонения клавиатуры.

Например, мне нравится такой подход, когда следующий код добавляется в любой ViewController:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"* * * * * * * * *ViewControllerBase touchesBegan");

    [self.view endEditing:YES]; // dismiss the keyboard

    [super touchesBegan:touches withEvent:event];
}

... но этот метод не имеет отношения к ситуациям, когда, например, касание происходит в отображаемом UITableView. Поэтому мне нужно добавить некоторый код для вызова endEditing при endEditing UITableView и т.д. И т.д. И т.д. Это означает, что мое приложение будет обильно обсыпано большим количеством кода, чтобы закрыть клавиатуру при касании различных других UIElements.

Я думаю, я мог бы просто попытаться определить все места, где нужно перехватить прикосновения и отклонить клавиатуру, но мне кажется, что где-то может быть лучший шаблон дизайна для обработки событий отклонения клавиатуры iOS.

Может ли кто-нибудь поделиться своим опытом в этом вопросе и порекомендовать конкретную технику для общей обработки отказа клавиатуры во всем приложении?

Большое спасибо

Ответ 1

Ваша иерархия просмотров живет внутри UIWindow. UIWindow отвечает за пересылку событий касания к правильному виду в своем методе sendEvent:. Позвольте сделать подкласс UIWindow для переопределения sendEvent:.

@interface MyWindow : UIWindow
@end

Окно будет нуждаться в ссылке на текущего первого ответчика, если таковой имеется. Вы также можете использовать UITextView, поэтому мы будем наблюдать уведомления как из текстовых полей, так и из текстовых представлений.

@implementation MyWindow {
    UIView *currentFirstResponder_;
}

- (void)startObservingFirstResponder {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(observeBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
    [center addObserver:self selector:@selector(observeEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
    [center addObserver:self selector:@selector(observeBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
    [center addObserver:self selector:@selector(observeEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
}

- (void)stopObservingFirstResponder {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil];
    [center removeObserver:self name:UITextFieldTextDidEndEditingNotification object:nil];
    [center removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
    [center removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
}

- (void)observeBeginEditing:(NSNotification *)note {
    currentFirstResponder_ = note.object;
}

- (void)observeEndEditing:(NSNotification *)note {
    if (currentFirstResponder_ == note.object) {
        currentFirstResponder_ = nil;
    }
}

Окно начнет наблюдать за уведомлениями при его инициализации и остановится при его освобождении:

- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self commonInit];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit {
    [self startObservingFirstResponder];
}

- (void)dealloc {
    [self stopObservingFirstResponder];
}

Мы переопределим sendEvent:, чтобы "настроить" первого ответчика на основе события, а затем вызовите super sendEvent:, чтобы отправить событие нормально.

- (void)sendEvent:(UIEvent *)event {
    [self adjustFirstResponderForEvent:event];
    [super sendEvent:event];
}

Нам не нужно ничего делать с первым ответчиком, если нет первого ответчика. Если есть первый ответчик, и он содержит прикосновение, мы не хотим заставить его уйти в отставку. (Помните, что может быть несколько прикосновений одновременно!) Если есть первый ответчик, и в другом представлении, которое может стать первым ответчиком, появляется новое прикосновение, система будет обрабатывать это правильно автоматически, поэтому мы также хотим проигнорировать этот случай. Но если есть первый ответчик, и он не содержит никаких касаний, и появляется новое прикосновение в представлении, которое не может стать первым ответчиком, мы хотим, чтобы первый ответчик ушел в отставку.

- (void)adjustFirstResponderForEvent:(UIEvent *)event {
    if (currentFirstResponder_
        && ![self eventContainsTouchInFirstResponder:event]
        && [self eventContainsNewTouchInNonresponder:event]) {
        [currentFirstResponder_ resignFirstResponder];
    }
}

Отчет о том, содержит ли событие касание в первом ответчике, легко:

- (BOOL)eventContainsTouchInFirstResponder:(UIEvent *)event {
    for (UITouch *touch in [event touchesForWindow:self]) {
        if (touch.view == currentFirstResponder_)
            return YES;
    }
    return NO;
}

Сообщение о том, содержит ли событие новое касание в представлении, которое не может стать первым ответчиком, почти так же легко:

- (BOOL)eventContainsNewTouchInNonresponder:(UIEvent *)event {
    for (UITouch *touch in [event touchesForWindow:self]) {
        if (touch.phase == UITouchPhaseBegan && ![touch.view canBecomeFirstResponder])
            return YES;
    }
    return NO;
}

@end

Как только вы внедрили этот класс, вам нужно изменить свое приложение, чтобы использовать его вместо UIWindow.

Если вы создаете UIWindow в application:didFinishLaunchingWithOptions:, вам нужно #import "MyWindow.h" в верхней части вашего AppDelegate.m, а затем изменить application:didFinishLaunchingWithOptions:, чтобы создать MyWindow вместо UIWindow.

Если вы создаете свой UIWindow в nib, вам нужно установить пользовательский класс окна MyWindow в nib.

Ответ 2

Вот гораздо более простой и эффективный способ справиться с этим. Это будет работать для любого UITextField в вашем контроллере вида. Вы даже можете добавить его в свой базовый контроллер (если он у вас есть), и он будет работать как шарм.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    UITouch *touch = [[event allTouches] anyObject];

    if (![[touch view] isKindOfClass:[UITextField class]]) {
        [self.view endEditing:YES];
    }
    [super touchesBegan:touches withEvent:event];
}

Ответ 3

Обычно я использую следующие

Во-первых:

Включите в свой файл реализации следующее расширение, findFirstResponder поможет вам найти первый резонанс

@implementation UIView (FindFirstResponder)
- (UIView*)findFirstResponder
{
    if (self.isFirstResponder) {
        return self;     
    }
    for (UIView *subView in self.subviews) {
        UIView *responder = [subView findFirstResponder];
        if (responder)
            return responder;
    }

    return nil;
}
@end

Затем в вашем представлении контроллер viewDidLoad добавьте следующие

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(keyboardDidShow:) 
                   name:UIKeyboardDidShowNotification 
                 object:nil];

    [center addObserver:self selector:@selector(keyboardDidHide:) 
                   name:UIKeyboardWillHideNotification 
                 object:nil];
    // Do any additional setup after loading the view, typically from a nib.
}

Функции уведомления будут такими, как

- (void) keyboardDidShow:(NSNotification*)notification
{    
    UIButton *button = [[UIButton alloc] init];

    CGRect rect = self.view.bounds;


    button.frame = rect;
    button.backgroundColor = [UIColor blackColor];
    button.tag = 111;
    UIView *currentResponer = [self.view findFirstResponder];
    [button addTarget:currentResponer action:@selector(resignFirstResponder) forControlEvents:UIControlEventTouchUpInside];

    [self.view insertSubview:button belowSubview:currentResponer];
}

- (void) keyboardDidHide:(NSNotification*)notification
{
    [[self.view viewWithTag:111] removeFromSuperview];
}

Когда клавиатура показывает, я добавляю UIButton под текущим первым ответчиком, это действие кнопки скроет клавиатуру,

Ограничение здесь заключается в том, что UITextField должно быть в self.view, однако вы можете адаптировать этот метод для своей потребности с некоторыми изменениями, надеясь, что это поможет вам

Ответ 4

Мне действительно нравится техника Омара - она ​​прекрасно работает, но я добавил пару строк в моем случае.

- (void)keyboardDidShow:(NSNotification*)notification
{
    UIButton *button = [[UIButton alloc] init];

    CGRect rect = self.view.bounds;

    button.frame = rect;
    button.backgroundColor = [UIColor blackColor];

    [button setAlpha:0.5f];
    button.tag = 111;
    UIView *currentResponder = [self.view findFirstResponder];

    [self.view bringSubviewToFront:currentResponder];

    if (currentResponder == _descriptionTextView) [_descriptionTextView setTextColor:[UIColor whiteColor]];

    [button addTarget:currentResponder action:@selector(resignFirstResponder) forControlEvents:UIControlEventTouchUpInside];

    [self.view insertSubview:button belowSubview:currentResponder];
}

Я добавил три строки к его решению (см. выше):

  • Установите альфа кнопки на 0.6, чтобы я мог видеть некоторые из моих viewController за ним.
  • Приведите текущий ответчик к фронту, так что ни один из моих других представлений не находится перед кнопкой.
  • Переключите цвет моего UITextView, чтобы текст был более четким (мой UITextView имеет четкий фон, поэтому текст не показывался ясно).

Во всяком случае, только незначительные изменения, но, надеюсь, эта помощь. Спасибо Омару.

Ответ 5

Чтобы отменить текущий первый ответчик, я попытался использовать endEditing, но у меня возникли проблемы с настройкой первого ответчика. Затем я использовал рекурсивный метод, чтобы найти первого ответчика в методе категории UIView:

- (UIView *)getFirstResponder {

    if (self.isFirstResponder) {
        return self;
    }

    for (UIView *subView in self.subviews) {
        UIView *firstResponder = [subView getFirstResponder];

        if (firstResponder != nil) {
            return firstResponder;
        }
    }

    return nil;
}

Как только ответчик будет возвращен, я могу просто вызвать resignFirstResponder на нем.

Только сегодня мой приятель показал мне еще лучший способ: http://overooped.com/post/28510603744/i-had-completely-forgotten-about-nil-targeted

Просто назовите это:

[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];

Удивительный один лайнер!

Ответ 6

В Swift 3

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    self.view.endEditing(true)
}

Ответ 7

Может быть, я не понимаю, что вы хотите правильно, но метод - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch может помочь вам отклонить клавиатуру

Ответ 8

Добавить метод уведомления textFieldDidChange в текстовое поле.

[textField addTarget: само действие: @selector (textFieldDidChange:) forControlEvents: UIControlEventEditingDidEnd];

работает для меня