Ottieni il primo risponditore corrente senza utilizzare un’API privata

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

L’API non pubblico inclusa nella tua applicazione è firstResponder.

Ora, la chiamata API offendente è 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 corrente sullo schermo? Sto cercando un modo per non respingere la mia app.

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 è basato su quello 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` } 

Se il tuo objective finale è semplicemente quello di dimettermi dal primo [self.view endEditing:YES] , questo dovrebbe funzionare: [self.view endEditing:YES]

Un modo comune di manipolare il primo risponditore è quello di utilizzare azioni mirate nil. Questo è un modo per inviare un messaggio arbitrario alla catena di risposta (a partire dal primo risponditore) e continuare lungo la catena finché qualcuno non risponde al messaggio (ha implementato un metodo che corrisponde al selettore).

Per il caso di chiudere la tastiera, questo è il modo più efficace che funzionerà indipendentemente da quale finestra o vista sia il primo a rispondere:

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

Questo dovrebbe essere più efficace di anche [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] . Basta aggiungere i seguenti due file al tuo progetto:

UIResponder + FirstResponder.h

 #import  @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 è bello, ma il modo in cui mi dimetto il firstResponder quando non so cosa sia il risponditore:

Creare un campo UIText, in IB o programmaticamente. Rendilo nascosto. Collegalo al tuo codice se lo hai fatto in IB.

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

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

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

Sfrutta il metodo di Apple sendAction: to: from: forEvent: che già sa come accedere al primo risponditore.

Abbiamo solo bisogno di modificarlo in 2 modi:

  • Estendi UIResponder modo che possa eseguire il nostro codice sul primo risponditore.
  • Sottoclass 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 

Per una versione di Swift 3 della risposta di nevyn :

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

Il primo risponditore può essere una qualsiasi istanza della class UIResponder, quindi ci sono altre classi che potrebbero essere le prime a rispondere nonostante l’UIViews. Ad esempio, UIViewController potrebbe anche essere il primo a rispondere.

In questo gist troverai un modo ricorsivo per ottenere il primo risponditore scorrendo la gerarchia dei controller a partire dal rootViewController delle windows dell’applicazione.

È quindi ansible recuperare il primo interlocutore

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

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

Per risolvere questo problema possiamo fare un approccio diverso creando una categoria su UIResponder ed eseguendo un po ‘di swizzeling magico per essere in grado di build una serie di tutte le istanze viventi di questa class. Quindi, per ottenere il primo risponditore, è ansible eseguire una semplice iterazione e chiedere a ciascun object if -isFirstResponder .

Questo approccio può essere trovato implementato in questo altro gist .

Spero che sia d’aiuto.

Usando Swift e con uno specifico object UIView ciò 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 } 

Basta posizionarlo nel tuo UIViewController e usarlo 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 sia stato trovato firstResponser nelle visualizzazioni delle viste fornite.

Iterate sulle viste che potrebbero essere le prime a rispondere e utilizzare - (BOOL)isFirstResponder per determinare se sono attualmente.

Peter Steinberger ha appena twittato sulla notifica privata UIWindowFirstResponderDidChangeNotification , che è ansible osservare se si desidera vedere la modifica del primoRispettore.

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

puoi aggiungere il riconoscimento del gesto Toccare nel file xib o storyboard e connetterlo a un’azione,

sembra qualcosa del genere quindi è finito

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

Questo è quello che ho fatto per trovare ciò che UITextField è il primoRispettore 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]; } } } } } 

Solo il caso qui è la versione di 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 nella mia categoria UIViewController. Utile per molte cose, tra cui 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 , è ansible chiedere legalmente all’object UIApplication di dirti chi è il primo risponditore.

Guarda questo:

C’è un modo per chiedere a una vista iOS quale dei suoi figli ha il primo stato di risponditore?

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 ha bisogno di un altro ciclo. Stavo lavorando con tableViewController. Ho modificato la sceneggiatura e poi ho controllato. Tutto ha funzionato perfettamente.

Mi raccomando 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 c’è bisogno di 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 privite api come questa, Apple ignora:

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

Vorrei condividere con voi la mia implementazione per trovare il primo risponditore in qualsiasi punto di UIView. Spero che aiuti 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 della risposta di @ 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 } } 

Il codice qui sotto 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. È ansible utilizzare UIApplication.shared.sendAction(_:to:from:for:) per chiamare il primo risponditore dimostrato in questo collegamento: http://stackoverflow.com/a/14135456/746890 .


La maggior parte delle risposte qui non riesce a trovare il primo risponditore corrente se non è nella gerarchia della vista. Ad esempio, sottoclass AppDelegate o UIViewController .

C’è un modo per garantire di trovarlo anche se il primo object responder non è un UIView .

Per prima cosa implementiamo una versione invertita di essa, usando la proprietà next di UIResponder :

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

Con questa proprietà calcasting, possiamo trovare il primo risponditore corrente dal basso verso l’alto anche se non è UIView . Ad esempio, da una view a UIViewController che la gestisce, se il controller di visualizzazione è il primo a rispondere.

Tuttavia, abbiamo ancora bisogno di una risoluzione top-down, una singola var per ottenere il primo risponditore corrente.

Innanzitutto con la gerarchia delle viste:

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

Questo cercherà il primo risponditore all’indietro, e se non fosse in grado di trovarlo, direbbe alle sue sottoview di fare la stessa cosa (perché la sua sottoview next non è necessariamente essa stessa). Con questo possiamo trovarlo da qualsiasi vista, inclusa UIWindow .

E infine, possiamo build questo:

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

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

 let firstResponder = UIResponder.first