Pregunta

Envié mi aplicación hace poco más de una semana y hoy recibí el temido correo electrónico de rechazo. 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 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 obtengo el primer respondedor actual en la pantalla? Estoy buscando una manera de que mi aplicación no sea rechazada.

¿Fue útil?

Solución

En una de mis aplicaciones, a menudo quiero que el primer respondedor renuncie si el usuario toca el fondo. Para este propósito escribí una categoría en UIView, que llamo en la UIWindow.

Lo siguiente se basa en eso y debería 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;
}

Swift:

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`
}

Otros consejos

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

Una forma común de manipular al primer respondedor es usar acciones específicas nulas. Esta es una forma de enviar un mensaje arbitrario a la cadena de respuesta (comenzando con el primer respondedor) y continuar por la cadena hasta que alguien responda al mensaje (ha implementado un método que coincida 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 la primera en responder:

[[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)

Aquí hay una categoría que le permite encontrar rápidamente el primer respondedor llamando a [UIResponder currentFirstResponder] . Simplemente agregue los siguientes dos archivos a su 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 cero lo envía al primer respondedor.

(Originalmente publiqué esta respuesta aquí: https://stackoverflow.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://stackoverflow.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
    }
}

No es bonito, pero la forma en que renuncio al FirstResponder cuando no sé cuál es el respondedor:

Cree un UITextField, ya sea en IB o mediante programación. Hazlo oculto. Vincúlelo a su código si lo hizo en IB.

Luego, cuando desea cerrar el teclado, cambia el respondedor al campo de texto invisible e inmediatamente lo renuncia:

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

Para un Swift 3 & amp; 4 versión de nevyn's respuesta:

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

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

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

Solo necesitamos ajustarlo 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

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 esencia encontrará una forma recursiva de obtener el primer respondedor recorriendo el jerarquía de controladores a partir del rootViewController de las ventanas de la aplicación.

Puede recuperar 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 al crear una categoría en UIResponder y realizar un poco de magia para poder construir una matriz de todas las instancias vivas de esta clase. Luego, para obtener la primera respuesta, podemos iterar y preguntar a cada objeto si -isFirstResponder .

Este enfoque se puede encontrar implementado en esta otra esencia .

Espero que ayude.

Usando Swift y con un objeto específico UIView 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 úselo 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 encuentre firstResponser en las subvistas de vistas dadas.

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

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

Si solo necesita eliminar 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 Tap en el archivo xib o storyboard y conectarlo a una acción,

se parece a esto y luego termina

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

Por si acaso, 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
    }

}

Esto es lo que hice para encontrar qué UITextField es el primer Respondedor 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, incluida la primera respuesta. ¡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;
 }];
}

Con una categoría en UIResponder , es posible pedir legalmente al objeto UIApplication que le diga quién es el primer respondedor.

Ver esto:

¿Hay alguna forma de preguntarle a una vista de iOS cuál de sus hijos tiene el estado de primer respondedor?

Puede elegir la siguiente extensión UIView para obtener (crédito de Daniel) :

extension UIView {
    var firstResponder: UIView? {
        guard !isFirstResponder else { return self }
        return subviews.first(where: {<*>.firstResponder != nil })
    }
}

También puedes intentarlo 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 intenté pero parece una buena solución

La solución de romeo https://stackoverflow.com/a/2799675/661022 es genial, pero me di cuenta que el código necesita un bucle más. Estaba trabajando con tableViewController. Edité el guión y luego lo verifiqué. 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]);
                        }
                    }
                }
            }
        }
    }
}

¡Este es un buen candidato para la recursividad! 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;
}

puedes llamar a una API privada como esta, Apple ignora:

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

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;

}

Versión rápida de @thomas -m & # 252; ller respuesta de

extension UIView {

    func firstResponder() -> UIView? {
        if self.isFirstResponder() {
            return self
        }

        for subview in self.subviews {
            if let firstResponder = subview.firstResponder() {
                return firstResponder
            }
        }

        return nil
    }

}

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

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
}

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

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


La mayoría de las respuestas aquí realmente 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 un UIView .

Primero, implementemos una versión invertida, utilizando la propiedad next 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, desde una view hasta el UIViewController que lo administra, si el controlador de vista es el primer respondedor.

Sin embargo, todavía necesitamos una resolución de arriba hacia abajo, una sola var para obtener el primer respondedor actual.

Primero con la jerarquía de vistas:

extension UIView {
    var previousFirstResponder: UIResponder? {
        return nextFirstResponder ?? subviews.compactMap { 
extension UIResponder {
    static var first: UIResponder? {
        return UIApplication.shared.windows.compactMap({ 
let firstResponder = UIResponder.first
.previousFirstResponder }).first } }
.previousFirstResponder }.first } }

Esto buscará el primer respondedor al revés, y si no puede encontrarlo, le dirá a sus subvistas que hagan lo mismo (porque el next de su subview no es necesariamente en sí mismo). Con esto podemos encontrarlo desde cualquier vista, incluida UIWindow .

Y finalmente, podemos construir esto:

<*>

Entonces, cuando desee recuperar el primer respondedor, puede llamar:

<*>
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top