Domanda

Ho inviato la mia app poco più di una settimana fa e ho ricevuto l'e-mail di rifiuto temuta oggi. Mi dice che la mia app non può essere accettata perché sto usando un'API non pubblica; in particolare, dice,

  

L'API non pubblica inclusa nella tua applicazione è firstResponder.

Ora, la chiamata API offensiva è in realtà una soluzione che ho trovato qui su SO:

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

Come posso ottenere il primo risponditore attuale sullo schermo? Sto cercando un modo per non rifiutare la mia app.

È stato utile?

Soluzione

In una delle mie applicazioni spesso desidero che il primo risponditore si dimetta se l'utente tocca lo sfondo. A tale scopo ho scritto una categoria su UIView, che chiamo su UIWindow.

Quanto segue si basa su questo e dovrebbe restituire il primo risponditore.

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

Esempio di utilizzo in Swift:

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

Altri suggerimenti

Se il tuo obiettivo finale è solo quello di dare le dimissioni al primo risponditore, dovrebbe funzionare: [self.view endEditing: YES]

Un modo comune di manipolare il primo soccorritore è usare zero azioni mirate. Questo è un modo per inviare un messaggio arbitrario alla catena di risponditori (a partire dal primo risponditore) e continuare lungo la catena fino a quando qualcuno non risponde al messaggio (ha implementato un metodo corrispondente al selettore).

Nel caso di chiudere la tastiera, questo è il modo più efficace che funzionerà indipendentemente dalla finestra o dalla vista che risponde per prima:

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

Questo dovrebbe essere più efficace persino di [self.view.window endEditing: YES] .

(Grazie a BigZaphod per avermi ricordato il concetto)

Ecco una categoria che ti consente di trovare rapidamente il primo risponditore chiamando [UIResponder currentFirstResponder] . Aggiungi i seguenti due file al tuo progetto:

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

Il trucco qui è che l'invio di un'azione a zero lo invia al primo risponditore.

(Originariamente ho pubblicato questa risposta qui: https://stackoverflow.com/a/14135456/322427 )

Ecco un'estensione implementata in Swift basata sulla risposta più eccellente di 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
    }
}

Non è carino, ma il modo in cui mi dimetto dal primo risponditore quando non so cosa sia il risponditore:

Crea un UITextField, in IB o programmaticamente. Rendilo nascosto. Collegalo al tuo codice se lo hai creato in IB.

Quindi, quando si desidera chiudere la tastiera, si passa al risponditore nel campo di testo invisibile e lo si dimette immediatamente:

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

Per uno Swift 3 & amp; 4 versione della la risposta di Nevyn :

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

Ecco una soluzione che riporta il primo risponditore corretto (molte altre soluzioni non segnalano un UIViewController come il primo risponditore, ad esempio), non richiede il ciclo sulla gerarchia della vista e non utilizzare API private.

Sfrutta il metodo Apple sendAction: a: da: forEvent: , che sa già come accedere al primo risponditore.

Dobbiamo solo modificarlo in 2 modi:

  • Estendi UIResponder in modo che possa eseguire il nostro codice sul primo risponditore.
  • Sottoclasse UIEvent per restituire il primo risponditore.

Ecco il codice:

@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

Il primo risponditore può essere qualsiasi istanza della classe UIResponder, quindi ci sono altre classi che potrebbero essere il primo risponditore nonostante le UIVview. Ad esempio UIViewController potrebbe anche essere il primo risponditore.

In questa sintesi troverai un modo ricorsivo per ottenere il primo soccorritore eseguendo il ciclo attraverso il gerarchia di controller a partire dal rootViewController delle finestre dell'applicazione.

Puoi recuperare il primo risponditore facendo

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

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

Tuttavia, se il primo risponditore non è una sottoclasse di UIView o UIViewController, questo approccio fallirà.

Per risolvere questo problema, possiamo adottare un approccio diverso creando una categoria su UIResponder ed eseguire alcuni swizzeling magici per essere in grado di costruire un array di tutte le istanze viventi di questa classe. Quindi, per ottenere il primo risponditore, possiamo semplicemente iterare e chiedere ad ogni oggetto se -isFirstResponder .

Questo approccio può essere trovato implementato in quest'altro aspetto .

Spero che sia d'aiuto.

L'uso di Swift e con un oggetto UIView specifico potrebbe aiutare:

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
}

Inseriscilo nel tuo UIViewController e usalo in questo modo:

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

Prendi nota del fatto che il risultato è un valore facoltativo , quindi sarà nullo nel caso in cui non fosse stato trovato il primo Responsabile nelle sottoview delle viste date.

Scorri le visualizzazioni che potrebbero essere il primo risponditore e usa - (BOOL) isFirstResponder per determinare se lo sono attualmente.

Peter Steinberger ha appena twittato sulla notifica privata UIWindowFirstResponderDidChangeNotification , che puoi osservare se vuoi vedere la prima modifica del Matcher.

Se devi solo uccidere la tastiera quando l'utente tocca un'area di sfondo, perché non aggiungere un riconoscimento gesti e usarlo per inviare il messaggio [[self view] endEditing: YES] ?

puoi aggiungere il Riconoscimento dei gesti dei tocchi nel file xib o storyboard e collegarlo a un'azione

sembra qualcosa del genere quindi finito

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

Solo qui è la versione Swift del fantastico approccio di 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
    }

}

Questo è quello che ho fatto per trovare quello che UITextField è il primoRisponditore quando l'utente fa clic su Salva / Annulla in 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];
                }

            }
        }

    }

}

Questo è quello che ho nella mia categoria UIViewController. Utile per molte cose, incluso ottenere il primo soccorritore. I blocchi sono fantastici!

- (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 categoria su UIResponder , è possibile chiedere legalmente l'oggetto UIApplication per dirti chi è il primo risponditore.

Vedi questo:

Esiste un modo per chiedere a una vista iOS quale dei suoi figli ha lo stato di primo risponditore?

Puoi scegliere la seguente estensione UIView per ottenerla (credito di Daniel) :

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

Puoi provare anche in questo modo:

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

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

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

Non l'ho provato ma sembra una buona soluzione

La soluzione di romeo https://stackoverflow.com/a/2799675/661022 è interessante, ma ho notato che il codice necessita di un altro loop. Stavo lavorando con tableViewController. Ho modificato lo script e poi ho controllato. Tutto ha funzionato perfettamente.

Ho raccomandato di provare questo:

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

Questo è un buon candidato per la ricorsione! Non è necessario aggiungere una categoria a UIView.

Utilizzo (dal controller della vista):

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

Codice:

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

puoi chiamare API private in questo modo, apple ignore:

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

Vorrei condividere con te la mia implementazione per trovare il primo soccorritore in qualsiasi parte di UIView. Spero sia d'aiuto e mi dispiace per il mio inglese. Grazie

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

}

Versione rapida di @thomas -müller la risposta

extension UIView {

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

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

        return nil
    }

}

Ho un approccio leggermente diverso rispetto alla maggior parte qui. Invece di scorrere la raccolta di visualizzazioni cercando quella che ha isFirstResponder impostato, invio anch'io un messaggio a nil , ma conservo il ricevitore (se presente), quindi restituisce quel valore in modo che il chiamante ottenga l'istanza effettiva (di nuovo, se presente).

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

Posso quindi dare le dimissioni dal primo risponditore, se presente, facendo questo ...

return UIResponder.first?.resignFirstResponder()

Ma ora posso anche fare questo ...

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

Il codice seguente funziona.

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

Aggiornamento: ho sbagliato. Puoi infatti utilizzare UIApplication.shared.sendAction (_: a: da: per :) per chiamare il primo risponditore dimostrato in questo link: http://stackoverflow.com/a/14135456/746890 .


La maggior parte delle risposte qui non riesce davvero a trovare l'attuale primo risponditore se non si trova nella gerarchia della vista. Ad esempio, le sottoclassi AppDelegate o UIViewController .

C'è un modo per garantirti di trovarlo anche se il primo oggetto responder non è un UIView .

Innanzitutto, consente di implementarne una versione invertita, utilizzando la proprietà next di UIResponder :

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

Con questa proprietà calcolata, possiamo trovare l'attuale primo risponditore dal basso verso l'alto anche se non è UIView . Ad esempio, da una view a UIViewController chi lo sta gestendo, se il controller di visualizzazione è il primo risponditore.

Tuttavia, abbiamo ancora bisogno di una risoluzione dall'alto verso il basso, un singolo var per ottenere l'attuale primo risponditore.

Prima con la gerarchia della vista:

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

Questo cercherà il primo risponditore all'indietro e, se non riuscisse a trovarlo, direbbe alle sue sottoview di fare la stessa cosa (perché il next della sua subview non è necessariamente se stesso). Con questo possiamo trovarlo da qualsiasi vista, incluso UIWindow .

E infine, possiamo costruirlo:

<*>

Quindi, quando vuoi recuperare il primo risponditore, puoi chiamare:

<*>
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top