Descartar el teclado al tocar en cualquier lugar fuera de UITextField

Estoy desarrollando una aplicación para iPad que tiene una gran cantidad de UIViewControllers, UITableViews (con celdas con accesoriasViews de UITextFields), etc. Muchos de los UIViewControllers aparecen dentro de una jerarquía de navegación.

Hay muchos lugares diferentes en los que aparecen UITextFields, incluidos como accesorias UITableViewCell.

Me gustaría diseñar una estrategia eficiente para descartar el teclado siempre que el usuario toque fuera del UITextField que se está editando actualmente. He buscado técnicas para descartar el teclado, pero todavía no he encontrado una respuesta que explique cómo podría funcionar una estrategia general de descarte del teclado.

Por ejemplo, me gusta este enfoque, donde se agrega el siguiente código a cualquier ViewController:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"* * * * * * * * *ViewControllerBase touchesBegan"); [self.view endEditing:YES]; // dismiss the keyboard [super touchesBegan:touches withEvent:event]; } 

… pero esta técnica no se ocupa de situaciones en las que, por ejemplo, se produce un toque dentro de una UITableView que está en exhibición. Por lo tanto, necesitaría agregar algún código para llamar a endEditing cuando se toque una UITableView, etc., etc. Lo que significa que mi aplicación se rociará generosamente con un montón de código para descartar el teclado cuando se toquen otros UIElements.

Supongo que podría tratar de identificar todos los diferentes lugares donde los toques deben ser interceptados y el teclado descartado, pero me parece que puede haber un mejor patrón de diseño en alguna parte para manejar los eventos de descarte de teclado de iOS.

¿Alguien puede compartir sus experiencias en este asunto y recomendar una técnica específica para manejar genéricamente el despido del teclado en toda una aplicación?

Muchas gracias

Su jerarquía de vistas vive dentro de una UIWindow . La UIWindow es responsable de reenviar eventos táctiles a la vista correcta en su método sendEvent: . Hagamos una subclase de UIWindow para anular sendEvent:

 @interface MyWindow : UIWindow @end 

La ventana necesitará una referencia al primer respondedor actual, si hay uno. También puede decidir usar UITextView , por lo que observaremos las notificaciones de los campos de texto y las vistas de texto.

 @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; } } 

La ventana comenzará a observar las notificaciones cuando se inicialice, y se detendrá cuando se desasigne:

 - (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: a sendEvent: para “ajustar” al primer respondedor basado en el evento, y luego llamaremos al evento sendEvent: de super sendEvent: para enviar el evento normalmente.

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

No necesitamos hacer nada sobre el primer respondedor si no hay un primer interviniente. Si hay un primer respondedor, y contiene un toque, no queremos obligarlo a renunciar. (¡Recuerde que puede haber toques múltiples al mismo tiempo!) Si hay un primer respondedor y aparece un nuevo toque en otra vista que puede convertirse en el primer respondedor, el sistema lo manejará correctamente automáticamente, por lo que también queremos ignorar ese caso. Pero si hay un primer respondedor, y no contiene ningún toque, y aparece un nuevo toque en una vista que no puede convertirse en el primer respondedor, queremos que el primer respondedor renuncie.

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

Informar si un evento contiene un toque en el primer respondedor es fácil:

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

Informar sobre si un evento contiene un nuevo toque en una vista que no puede convertirse en primera respuesta es casi tan fácil:

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

Una vez que haya implementado esta clase, debe cambiar su aplicación para usarla en lugar de UIWindow .

Si está creando su UIWindow en la application:didFinishLaunchingWithOptions: necesita #import "MyWindow.h" en la parte superior de su AppDelegate.m , y luego cambiar la application:didFinishLaunchingWithOptions: para crear una MyWindow lugar de una UIWindow .

Si está creando su UIWindow en un plumín, necesita establecer la clase personalizada de la ventana en MyWindow en el plumín.

Aquí hay una manera mucho más fácil y eficiente de lidiar con eso. Esto funcionará para cualquier UITextField en su controlador de vista. Incluso puede agregarlo a su controlador de vista base (si tiene uno) y funcionará como un amuleto.

 - (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]; } 

Normalmente uso el siguiente

Primero:

Incluya la siguiente extensión en su archivo de implementación, findFirstResponder lo ayudará a encontrar el primer resonador

 @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 

Luego, en su controlador de vista viewDidLoad agregue lo siguiente

 - (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. } 

Las funciones de notificación serán así

 - (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]; } 

Cuando se muestra el teclado, agrego un UIButton debajo del primer respondedor actual, esta acción del botón ocultará el teclado,

Una limitación aquí es que el UITextField tiene que estar en self.view Sin embargo, puede adaptar esta técnica para su necesidad con algunas modificaciones, espero que le ayude

Realmente me gusta la técnica de Omar, funciona muy bien, pero he agregado un par de líneas en mi caso.

 - (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]; } 

He agregado tres líneas a su solución (ver arriba):

  1. Establezca el alfa del botón en 0.6, de modo que pueda ver algo de mi viewController detrás de él.
  2. Ponga el actualResponder al frente, de modo que ninguna de mis otras vistas esté frente al botón.
  3. Cambie el color de mi UITextView para que el texto sea más claro (mi UITextView tiene un fondo claro, por lo que el texto no se muestra claramente).

De todos modos, solo cambios menores, pero espero que esto ayude. Gracias Omar.

Para renunciar a la primera respuesta actual, intenté usar endEditing, pero tuve algunos problemas al configurar una primera respuesta luego. Luego usé un método recursivo por un tiempo para encontrar el primer respondedor en un método de categoría 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; } 

Una vez que se devuelve el respondedor, puedo simplemente llamar a resignFirstResponder en él.

Sin embargo, hoy en día, un colega mío en el trabajo me mostró una mejor manera aquí: http://overooped.com/post/28510603744/i-had-completely-forgotten-about-nil-targeted

Solo llama esto:

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

Impresionante un trazador de líneas!

En Swift 3

 override func touchesBegan(_ touches: Set, with event: UIEvent?) { self.view.endEditing(true) } 

Puede ser que no entienda lo que quiere correctamente, pero - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch método - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch puede ayudarle a descartar el teclado

Agregue un método de notificación “textFieldDidChange” al control del campo de texto.

[textField addTarget: acción propia: @selector (textFieldDidChange 🙂 forControlEvents: UIControlEventEditingDidEnd];

funciona para mi