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;
}
Con una categoría en UIResponder
, es posible pedir legalmente al objeto UIApplication
que le diga quién es el primer respondedor.
Mira esto:
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
}
}