web-dev-qa-db-fra.com

Obtenir le premier répondant actuel sans utiliser une API privée

J'ai soumis mon application il y a un peu plus d'une semaine et j'ai reçu l'e-mail de rejet tant redouté aujourd'hui. Cela me dit que mon application ne peut pas être acceptée car j'utilise une API non publique; spécifiquement, il dit,

L'API non publique incluse dans votre application est firstResponder.

Maintenant, l'appel d'API incriminé est en fait une solution que j'ai trouvée ici sur SO:

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

Comment faire pour que le premier répondant actuel soit affiché à l'écran? Je cherche un moyen de ne pas rejeter mon application.

329
Justin Kredible

Dans l'une de mes applications, je souhaite souvent que le premier répondant démissionne si l'utilisateur tape sur l'arrière-plan. À cette fin, j'ai écrit une catégorie sur UIView, que j'appelle sur UIWindow.

Ce qui suit est basé sur cela et devrait renvoyer le premier répondant.

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

Exemple d'utilisation dans Swift:

if let firstResponder = view.window?.firstResponder {
    // do something with `firstResponder`
}
327
Thomas Müller

Si votre objectif ultime est simplement de démissionner du premier répondant, cela devrait fonctionner: [self.view endEditing:YES]

528
cdyson37

Une façon courante de manipuler le premier répondant consiste à utiliser des actions nulles ciblées. C'est un moyen d'envoyer un message arbitraire à la chaîne de répondeurs (en commençant par le premier répondant) et de poursuivre la chaîne jusqu'à ce que quelqu'un réponde au message (a mis en œuvre une méthode correspondant au sélecteur).

Pour le cas de la suppression du clavier, c'est le moyen le plus efficace qui fonctionne quelle que soit la fenêtre ou la vue qui est le premier intervenant:

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

Cela devrait être plus efficace que même [self.view.window endEditing:YES].

(Merci à BigZaphod de me rappeler le concept)

181
nevyn

Voici une catégorie qui vous permet de trouver rapidement le premier intervenant en appelant [UIResponder currentFirstResponder]. Ajoutez simplement les deux fichiers suivants à votre projet:

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

L'astuce ici est que l'envoi d'une action à nil l'envoie au premier répondant.

(J'ai initialement publié cette réponse ici: https://stackoverflow.com/a/14135456/322427 )

93
Jakob Egger

Voici une extension implémentée dans Swift basée sur l'excellente réponse 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
    }
}
36
Code Commander

Ce n'est pas joli, mais la façon dont j'ai démissionné du premier répondeur quand je ne sais pas ce qu'est le répondeur:

Créez un UITextField, dans IB ou par programme. Faites le caché. Associez-le à votre code si vous l'avez créé dans IB.

Ensuite, lorsque vous souhaitez ignorer le clavier, vous basculez le répondeur sur le champ de texte invisible et vous le résiliez immédiatement:

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

Pour une Swift version 3 & 4 de nevyn's réponse:

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

Voici une solution qui indique le premier répondant correct (de nombreuses autres solutions ne signalent pas un UIViewController comme premier répondant, par exemple), ne nécessitent pas de boucle sur la hiérarchie des vues et n'utilisent pas d'API privées.

Il exploite la méthode d'Apple sendAction: to: from: forEvent: , qui sait déjà comment accéder au premier répondant.

Nous avons juste besoin de le modifier de deux manières:

  • Étendez UIResponder pour qu'il puisse exécuter notre propre code sur le premier répondant.
  • Sous-classe UIEvent afin de renvoyer le premier répondant.

Voici le code:

@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
10
Senseful

Le premier répondant peut être n'importe quelle instance de la classe UIResponder. Il existe donc d'autres classes pouvant être le premier à répondre malgré les vues UIV. Par exemple, UIViewController pourrait également être le premier intervenant.

Dans this Gist , vous trouverez un moyen récursif d'obtenir le premier répondant en parcourant la hiérarchie des contrôleurs à partir du rootViewController des fenêtres de l'application.

Vous pouvez alors récupérer le premier répondant en faisant

- (void)foo
{
    // Get the first responder
    id firstResponder = [UIResponder firstResponder];

    // Do whatever you want
    [firstResponder resignFirstResponder];      
}

Cependant, si le premier répondant n'est pas une sous-classe de UIView ou UIViewController, cette approche échouera.

Pour résoudre ce problème, nous pouvons faire une approche différente en créant une catégorie sur UIResponder et en effectuant quelques opérations magiques pour pouvoir créer un tableau de toutes les instances vivantes de cette classe. Ensuite, pour obtenir le premier intervenant, nous pouvons simplement effectuer une itération et demander à chaque objet si -isFirstResponder.

Cette approche peut être trouvée implémentée dans cet autre Gist .

J'espère que ça aide.

7
vilanovi

Avec Swift et avec un objet UIView spécifique, cela pourrait aider:

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
}

Placez-le simplement dans votre UIViewController et utilisez-le comme ceci:

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

Prenez note que le résultat est un valeur facultative, donc il sera nul si aucune firstResponser n'a été trouvée dans les vues sous-vues données.

6
Dschee

Parcourez les vues qui pourraient être le premier intervenant et utilisez - (BOOL)isFirstResponder pour déterminer si elles le sont actuellement.

5
Johan Kool

Peter Steinberger vient de tweeted à propos de la notification privée UIWindowFirstResponderDidChangeNotification, que vous pouvez observer si vous souhaitez regarder le premier répondeur changer.

4
Heath Borders

Juste le cas ici est la version Swift de l’impressionnant approche 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
    }

}
4
Valentin Shergin

Si vous avez juste besoin de tuer le clavier lorsque l'utilisateur appuie sur une zone d'arrière-plan, pourquoi ne pas ajouter un identificateur de mouvements et l'utiliser pour envoyer le message [[self view] endEditing:YES]?

vous pouvez ajouter le dispositif de reconnaissance de gestes Taper dans le fichier xib ou storyboard et le connecter à une action,

ressemble à ceci puis fini

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

C'est ce que j'ai fait pour trouver quel UITextField est le premier répondeur lorsque l'utilisateur clique sur Enregistrer/Annuler dans 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];
                }

            }
        }

    }

}
3
Romeo

C'est ce que j'ai dans ma catégorie UIViewController. Utile pour beaucoup de choses, y compris pour obtenir le premier répondant. Les blocs sont super!

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

Avec une catégorie sur UIResponder, il est possible de demander légalement à l'objet UIApplication de vous dire qui est le premier répondant.

Regarde ça:

Y a-t-il un moyen de demander à une vue iOS lequel de ses enfants a le statut de premier répondant?

2
VJK

Vous pouvez choisir l’extension suivante UIView pour l’obtenir (crédit de Daniel):

extension UIView {
    var firstResponder: UIView? {
        guard !isFirstResponder else { return self }
        return subviews.first(where: {$0.firstResponder != nil })
    }
}
2
Soheil Novinfard

Vous pouvez essayer aussi comme ça:

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

    for (id textField in self.view.subviews) {

        if ([textField isKindOfClass:[UITextField class]] && [textField isFirstResponder]) {
            [textField resignFirstResponder];
        }
    }
} 

Je ne l'ai pas essayé mais cela me semble une bonne solution

1
Frade

vous pouvez appeler une API privée comme ceci, Apple ignore:

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

C'est un bon candidat pour la récursion! Pas besoin d'ajouter une catégorie à UIView.

Utilisation (à partir de votre contrôleur de vue):

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

Code:

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

La solution de romeo https://stackoverflow.com/a/2799675/661022 est cool, mais j'ai remarqué que le code nécessite une boucle de plus. Je travaillais avec tableViewController. J'ai édité le script et ensuite j'ai vérifié. Tout a fonctionné parfaitement.

J'ai recommandé d'essayer ceci:

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

J'ai une approche légèrement différente de la plupart des gens ici. Plutôt que de parcourir la collection de vues à la recherche de celle qui a isFirstResponder définie, j'envoie aussi un message à nil, mais je stocke le récepteur (le cas échéant), puis renvoie cette valeur pour que l'appelant obtient l'instance réelle (encore une fois, le cas échéant).

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

Je peux alors démissionner du premier intervenant, le cas échéant, en procédant ainsi ...

return UIResponder.first?.resignFirstResponder()

Mais je peux maintenant aussi le faire ...

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

Je voudrais partager avec vous mon implémentation pour trouver le premier répondant dans n’importe où dans UIView. J'espère que cela aide et désolé pour mon anglais. Merci

+ (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;

}

1
Rubens Iotti

Version rapide de la réponse 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
    }

}
1
Kádi

Dans Swift 5, il existe l'attribut facultatif "current" dans la classe UIResponder que vous pouvez utiliser pour obtenir le répondeur actuellement actif. Ressemble à ça:

UIResponder.current?.resignFirstResponder()

0
imattice

Mise à jour: je me suis trompé. Vous pouvez en effet utiliser UIApplication.shared.sendAction(_:to:from:for:) pour appeler le premier intervenant démontré dans ce lien: http://stackoverflow.com/a/14135456/74689 .


La plupart des réponses ne permettent pas de trouver le premier répondant actuel si ce n’est pas dans la hiérarchie des vues. Par exemple, AppDelegate ou UIViewController sous-classes.

Il existe un moyen de vous garantir de le trouver même si l'objet du premier répondeur n'est pas un UIView.

Commençons par implémenter une version inversée de celle-ci, en utilisant la propriété next de UIResponder:

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

Avec cette propriété calculée, nous pouvons trouver le premier répondant actuel de bas en haut même si ce n'est pas UIView. Par exemple, d'un view au UIViewController qui le gère, si le contrôleur de vue est le premier répondant.

Cependant, nous avons toujours besoin d’une résolution descendante, d’un seul var pour obtenir le premier répondant actuel.

D'abord avec la hiérarchie de vues:

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

Cela cherchera le premier intervenant à l'envers, et s'il ne peut pas le trouver, il dira à ses sous-vues de faire la même chose (parce que la sous-vue next n'est pas nécessairement elle-même). Avec cela, nous pouvons le trouver à partir de n’importe quelle vue, y compris UIWindow.

Et enfin, nous pouvons construire ceci:

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

Ainsi, lorsque vous souhaitez récupérer le premier intervenant, vous pouvez appeler:

let firstResponder = UIResponder.first
0
yesleon

Code ci-dessous travail.

- (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;
}   
0
John Chen