objective-c cocoa-touch first-responder

objective c - Obtén el primer respondedor actual sin usar una API privada



objective-c cocoa-touch (26)

¡Este es un buen candidato para la recursión! No es necesario agregar una categoría a UIView.

Uso (desde su controlador de vista):

UIView *firstResponder = [self findFirstResponder:[self view]];

Código:

// This is a recursive function - (UIView *)findFirstResponder:(UIView *)view { if ([view isFirstResponder]) return view; // Base case for (UIView *subView in [view subviews]) { if ([self findFirstResponder:subView]) return subView; // Recursion } return nil; }

Presenté mi aplicación hace poco más de una semana y recibí el temido correo electrónico de rechazo hoy. Me dice que mi aplicación no puede ser aceptada porque estoy usando una API no pública; específicamente, dice,

La API no pública que se incluye en su aplicación es firstResponder.

Ahora, la llamada a la API ofensiva es en realidad una solución que encontré aquí en SO:

UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; UIView *firstResponder = [keyWindow performSelector:@selector(firstResponder)];

¿Cómo consigo el primer respondedor actual en la pantalla? Estoy buscando una manera de que mi aplicación no sea rechazada.


Actualización: me equivoqué. De hecho, puede usar UIApplication.shared.sendAction(_:to:from:for:) para llamar al primer respondedor demostrado en este enlace: .com/a/14135456/746890 .

La mayoría de las respuestas aquí no pueden encontrar el primer respondedor actual si no está en la jerarquía de vistas. Por ejemplo, las subclases AppDelegate o UIViewController .

Hay una manera de garantizar que lo encuentre, incluso si el primer objeto de respuesta no es una vista UIView .

Primero, implementemos una versión invertida de la misma, utilizando la next propiedad de UIResponder :

extension UIResponder { var nextFirstResponder: UIResponder? { return isFirstResponder ? self : next?.nextFirstResponder } }

Con esta propiedad calculada, podemos encontrar el primer respondedor actual de abajo hacia arriba, incluso si no es UIView . Por ejemplo, de una view al UIViewController que la está administrando, si el controlador de la vista es el primer respondedor.

Sin embargo, todavía necesitamos una resolución descendente, una única var para obtener el primer respondedor actual.

Primero con la jerarquía de vistas:

extension UIView { var previousFirstResponder: UIResponder? { return nextFirstResponder ?? subviews.compactMap { $0.previousFirstResponder }.first } }

Esto buscará al primer respondedor hacia atrás, y si no pudiera encontrarlo, le diría a sus subvistas que hagan lo mismo (porque la next subvista no es necesariamente la misma). Con esto podemos encontrarlo desde cualquier vista, incluyendo UIWindow .

Y finalmente, podemos construir esto:

extension UIResponder { static var first: UIResponder? { return UIApplication.shared.windows.compactMap({ $0.previousFirstResponder }).first } }

Así que cuando quieras recuperar el primer respondedor, puedes llamar a:

let firstResponder = UIResponder.first


Aquí hay una categoría que le permite encontrar rápidamente al primer respondedor llamando a [UIResponder currentFirstResponder] . Solo agrega los siguientes dos archivos a tu proyecto:

UIResponder + FirstResponder.h

#import <Cocoa/Cocoa.h> @interface UIResponder (FirstResponder) +(id)currentFirstResponder; @end

UIResponder + FirstResponder.m

#import "UIResponder+FirstResponder.h" static __weak id currentFirstResponder; @implementation UIResponder (FirstResponder) +(id)currentFirstResponder { currentFirstResponder = nil; [[UIApplication sharedApplication] sendAction:@selector(findFirstResponder:) to:nil from:nil forEvent:nil]; return currentFirstResponder; } -(void)findFirstResponder:(id)sender { currentFirstResponder = self; } @end

El truco aquí es que enviar una acción a nil lo envía al primer respondedor.

(Originalmente publiqué esta respuesta aquí: https://.com/a/14135456/322427 )


Aquí hay una extensión implementada en Swift basada en la respuesta más excelente de Jakob Egger:

import UIKit extension UIResponder { // Swift 1.2 finally supports static vars!. If you use 1.1 see: // http://.com/a/24924535/385979 private weak static var _currentFirstResponder: UIResponder? = nil public class func currentFirstResponder() -> UIResponder? { UIResponder._currentFirstResponder = nil UIApplication.sharedApplication().sendAction("findFirstResponder:", to: nil, from: nil, forEvent: nil) return UIResponder._currentFirstResponder } internal func findFirstResponder(sender: AnyObject) { UIResponder._currentFirstResponder = self } }

Swift 4

import UIKit extension UIResponder { private weak static var _currentFirstResponder: UIResponder? = nil public static var current: UIResponder? { UIResponder._currentFirstResponder = nil UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil) return UIResponder._currentFirstResponder } @objc internal func findFirstResponder(sender: AnyObject) { UIResponder._currentFirstResponder = self } }


Aquí hay una solución que informa el primer respondedor correcto (muchas otras soluciones no informarán a un UIViewController como el primer respondedor, por ejemplo), no requiere realizar un bucle en la jerarquía de vistas y no utiliza API privadas.

Aprovecha el método de Apple sendAction:to:from:forEvent: que ya sabe cómo acceder al primer respondedor.

Solo tenemos que modificarlo de 2 maneras:

  • Extienda UIResponder para que pueda ejecutar nuestro propio código en el primer respondedor.
  • Subclase UIEvent para devolver el primer respondedor.

Aquí está el código:

@interface ABCFirstResponderEvent : UIEvent @property (nonatomic, strong) UIResponder *firstResponder; @end @implementation ABCFirstResponderEvent @end @implementation UIResponder (ABCFirstResponder) - (void)abc_findFirstResponder:(id)sender event:(ABCFirstResponderEvent *)event { event.firstResponder = self; } @end @implementation ViewController + (UIResponder *)firstResponder { ABCFirstResponderEvent *event = [ABCFirstResponderEvent new]; [[UIApplication sharedApplication] sendAction:@selector(abc_findFirstResponder:event:) to:nil from:nil forEvent:event]; return event.firstResponder; } @end


Código debajo del trabajo.

- (id)ht_findFirstResponder { //ignore hit test fail view if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) { return nil; } if ([self isKindOfClass:[UIControl class]] && [(UIControl *)self isEnabled] == NO) { return nil; } //ignore bound out screen if (CGRectIntersectsRect(self.frame, [UIApplication sharedApplication].keyWindow.bounds) == NO) { return nil; } if ([self isFirstResponder]) { return self; } for (UIView *subView in self.subviews) { id result = [subView ht_findFirstResponder]; if (result) { return result; } } return nil; }



El primer respondedor puede ser cualquier instancia de la clase UIResponder, por lo que hay otras clases que podrían ser el primer respondedor a pesar de las UIViews. Por ejemplo, UIViewController también podría ser el primer respondedor.

En esta lista encontrará una forma recursiva de obtener el primer respondedor haciendo un bucle a través de la jerarquía de controladores a partir del controlador de control de raíz de las ventanas de la aplicación.

Puede recuperar entonces el primer respondedor haciendo

- (void)foo { // Get the first responder id firstResponder = [UIResponder firstResponder]; // Do whatever you want [firstResponder resignFirstResponder]; }

Sin embargo, si el primer respondedor no es una subclase de UIView o UIViewController, este enfoque fallará.

Para solucionar este problema, podemos hacer un enfoque diferente mediante la creación de una categoría en UIResponder y realizar algunos swizzeling mágicos para poder construir una matriz de todas las instancias vivas de esta clase. Luego, para obtener el primer respondedor, podemos iterar y preguntar a cada objeto si -isFirstResponder .

Este enfoque se puede encontrar implementado en esta otra esencia .

Espero eso ayude.


En este caso, aquí está la versión Swift del increíble enfoque de Jakob Egger:

import UIKit private weak var currentFirstResponder: UIResponder? extension UIResponder { static func firstResponder() -> UIResponder? { currentFirstResponder = nil UIApplication.sharedApplication().sendAction(#selector(self.findFirstResponder(_:)), to: nil, from: nil, forEvent: nil) return currentFirstResponder } func findFirstResponder(sender: AnyObject) { currentFirstResponder = self } }


En una de mis aplicaciones a menudo quiero que el primer respondedor renuncie si el usuario pulsa en segundo plano. Para este propósito escribí una categoría en UIView, que invoco en UIWindow.

Lo siguiente se basa en eso y debe devolver el primer respondedor.

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

iOS 7+

- (id)findFirstResponder { if (self.isFirstResponder) { return self; } for (UIView *subView in self.view.subviews) { if ([subView isFirstResponder]) { return subView; } } return nil; }

Rápido:

extension UIView { var firstResponder: UIView? { guard !isFirstResponder else { return self } for subview in subviews { if let firstResponder = subview.firstResponder { return firstResponder } } return nil } }

Ejemplo de uso en Swift:

if let firstResponder = view.window?.firstResponder { // do something with `firstResponder` }


Esto es lo que hice para encontrar lo que UITextField es la primera respuesta cuando el usuario hace clic en Guardar / Cancelar en un ModalViewController:

NSArray *subviews = [self.tableView subviews]; for (id cell in subviews ) { if ([cell isKindOfClass:[UITableViewCell class]]) { UITableViewCell *aCell = cell; NSArray *cellContentViews = [[aCell contentView] subviews]; for (id textField in cellContentViews) { if ([textField isKindOfClass:[UITextField class]]) { UITextField *theTextField = textField; if ([theTextField isFirstResponder]) { [theTextField resignFirstResponder]; } } } } }


Esto es lo que tengo en mi categoría UIViewController. Útil para muchas cosas, incluyendo obtener el primer respondedor. ¡Los bloques son geniales!

- (UIView*) enumerateAllSubviewsOf: (UIView*) aView UsingBlock: (BOOL (^)( UIView* aView )) aBlock { for ( UIView* aSubView in aView.subviews ) { if( aBlock( aSubView )) { return aSubView; } else if( ! [ aSubView isKindOfClass: [ UIControl class ]] ){ UIView* result = [ self enumerateAllSubviewsOf: aSubView UsingBlock: aBlock ]; if( result != nil ) { return result; } } } return nil; } - (UIView*) enumerateAllSubviewsUsingBlock: (BOOL (^)( UIView* aView )) aBlock { return [ self enumerateAllSubviewsOf: self.view UsingBlock: aBlock ]; } - (UIView*) findFirstResponder { return [ self enumerateAllSubviewsUsingBlock:^BOOL(UIView *aView) { if( [ aView isFirstResponder ] ) { return YES; } return NO; }]; }


Iterar sobre las vistas que podrían ser el primer respondedor y usar - (BOOL)isFirstResponder para determinar si están actualmente.


La solución de romeo https://.com/a/2799675/661022 es genial, pero noté que el código necesita un bucle más. Estaba trabajando con tableViewController. Edité el guión y luego lo comprobé. Todo funcionó a la perfección.

Recomiendo probar esto:

- (void)findFirstResponder { NSArray *subviews = [self.tableView subviews]; for (id subv in subviews ) { for (id cell in [subv subviews] ) { if ([cell isKindOfClass:[UITableViewCell class]]) { UITableViewCell *aCell = cell; NSArray *cellContentViews = [[aCell contentView] subviews]; for (id textField in cellContentViews) { if ([textField isKindOfClass:[UITextField class]]) { UITextField *theTextField = textField; if ([theTextField isFirstResponder]) { NSLog(@"current textField: %@", theTextField); NSLog(@"current textFields''s superview: %@", [theTextField superview]); } } } } } } }


Me gustaría compartir con ustedes mi implementación para encontrar el primer respondedor en cualquier lugar de UIView. Espero que ayude y lo siento por mi inglés. Gracias

+ (UIView *) findFirstResponder:(UIView *) _view { UIView *retorno; for (id subView in _view.subviews) { if ([subView isFirstResponder]) return subView; if ([subView isKindOfClass:[UIView class]]) { UIView *v = subView; if ([v.subviews count] > 0) { retorno = [self findFirstResponder:v]; if ([retorno isFirstResponder]) { return retorno; } } } } return retorno;

}


No es bonito, pero la forma en que renuncio al primer Contestador cuando no sé lo que el respondedor es:

Cree un UITextField, ya sea en IB o programáticamente. Hazlo oculto. Enlázalo a tu código si lo hiciste en IB.

Luego, cuando desea descartar el teclado, cambia el respondedor al campo de texto invisible y lo abandona de inmediato:

[self.invisibleField becomeFirstResponder]; [self.invisibleField resignFirstResponder];


Para una versión Swift 3 y 4 de nevyn''s respuesta de nevyn''s :

UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)


Peter Steinberger acaba de tweeted sobre la notificación privada UIWindowFirstResponderDidChangeNotification , que puede observar si desea ver el primer cambio de respuesta.


Puedes llamar api privado como este, ignorar manzana:

UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; SEL sel = NSSelectorFromString(@"firstResponder"); UIView *firstResponder = [keyWindow performSelector:sel];


Puedes probar también así:

- (void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event { for (id textField in self.view.subviews) { if ([textField isKindOfClass:[UITextField class]] && [textField isFirstResponder]) { [textField resignFirstResponder]; } } }

No lo probé pero me parece una buena solución.


Si solo necesita matar el teclado cuando el usuario toca un área de fondo, ¿por qué no agrega un reconocedor de gestos y lo usa para enviar el mensaje [[self view] endEditing:YES] ?

puede agregar el reconocedor de gestos de toque en el archivo xib o storyboard y conectarlo a una acción,

se ve algo como esto y luego terminó

- (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer{ [[self view] endEditing:YES]; }


Si su objetivo final es simplemente renunciar al primer respondedor, esto debería funcionar: [self.view endEditing:YES]


Tengo un enfoque ligeramente diferente al de la mayoría aquí. En lugar de iterar a través de la colección de vistas buscando la que tiene isFirstResponder set, yo también envío un mensaje a nil , pero isFirstResponder el receptor (si lo hay), luego devuelvo ese valor para que la persona que llama obtenga la instancia real (de nuevo, si alguna).

import UIKit private var _foundFirstResponder: UIResponder? = nil extension UIResponder{ static var first:UIResponder?{ // Sending an action to ''nil'' implicitly sends it to the // first responder, where we simply capture it for return UIApplication.shared.sendAction(#selector(UIResponder.storeFirstResponder(_:)), to: nil, from: nil, for: nil) // The following ''defer'' statement executes after the return // While I could use a weak ref and eliminate this, I prefer a // hard ref which I explicitly clear as it''s better for race conditions defer { _foundFirstResponder = nil } return _foundFirstResponder } @objc func storeFirstResponder(_ sender: AnyObject) { _foundFirstResponder = self } }

Entonces puedo renunciar al primer respondedor, si lo hay, haciendo esto ...

return UIResponder.first?.resignFirstResponder()

Pero ahora también puedo hacer esto ...

if let firstResponderTextField = UIResponder?.first as? UITextField { // bla }


Una forma común de manipular al primer respondedor es utilizar acciones dirigidas nulas. Esta es una forma de enviar un mensaje arbitrario a la cadena de respondedores (comenzando con el primer respondedor) y continuar por la cadena hasta que alguien responda al mensaje (ha implementado un método que coincide con el selector).

Para el caso de descartar el teclado, esta es la forma más efectiva que funcionará sin importar qué ventana o vista sea el primer respondedor:

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

Esto debería ser más efectivo que incluso [self.view.window endEditing:YES] .

(Gracias a BigZaphod por recordarme el concepto)


Utilizando Swift y con un objeto UIView específico, esto podría ayudar:

func findFirstResponder(inView view: UIView) -> UIView? { for subView in view.subviews as! [UIView] { if subView.isFirstResponder() { return subView } if let recursiveSubView = self.findFirstResponder(inView: subView) { return recursiveSubView } } return nil }

Simplemente colóquelo en su UIViewController y utilícelo así:

let firstResponder = self.findFirstResponder(inView: self.view)

Tenga en cuenta que el resultado es un valor opcional, por lo que será nulo en caso de que no se haya encontrado firstResponser en las subvistas de vistas dadas.


Versión rápida de la respuesta de @thomas-müller

extension UIView { func firstResponder() -> UIView? { if self.isFirstResponder() { return self } for subview in self.subviews { if let firstResponder = subview.firstResponder() { return firstResponder } } return nil } }