Obter o atual primeiro respondedor sem usar uma API privada
-
22-07-2019 - |
Pergunta
Eu enviei meu aplicativo um pouco mais de uma semana atrás e conseguimos o email rejeição temida hoje. Ele me diz que meu aplicativo não pode ser aceite porque estou usando uma API não pública; Especificamente, ele diz:
A API não-pública que está incluído na sua aplicação é firstResponder.
Agora, a chamada API ofender é realmente uma solução que eu encontrei aqui no SO:
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *firstResponder = [keyWindow performSelector:@selector(firstResponder)];
Como faço para obter o socorrista atual na tela? Eu estou procurando uma maneira que não será meu aplicativo rejeitado.
Solução
Em uma das minhas aplicações muitas vezes eu quero que o socorrista a demitir-se as torneiras de usuário no fundo. Para este efeito, eu escrevi uma categoria em UIView, que eu chamo no UIWindow.
O seguinte é baseado em que e deve retornar o socorrista.
@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
}
}
Exemplo de uso em Swift:
if let firstResponder = view.window?.firstResponder {
// do something with `firstResponder`
}
Outras dicas
Se o seu objectivo final é apenas a demitir o primeiro respondedor, isso deve funcionar: [self.view endEditing:YES]
Uma maneira comum de manipular o socorrista é usar nil acções específicas. Esta é uma forma de enviar uma mensagem arbitrária para a cadeia de responder (começando com o primeiro respondedor), e continuando abaixo na cadeia até que alguém responde à mensagem (implementou um método correspondente ao selector).
Para o caso de demitir o teclado, esta é a maneira mais eficaz que irá funcionar não importa qual janela ou visão é socorrista:
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
Isto deve ser mais eficaz do que [self.view.window endEditing:YES]
mesmo.
(graças a BigZaphod por me lembrar do conceito)
Aqui está uma categoria que permite que você encontre rapidamente o socorrista chamando [UIResponder currentFirstResponder]
. Basta adicionar os seguintes dois arquivos ao seu projeto:
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
O truque aqui é que o envio de uma ação para nil envia para o socorrista.
(I originalmente publicado esta resposta aqui: https://stackoverflow.com/a/14135456/322427 )
Aqui está uma extensão implementado em Swift com base na resposta mais excelente do 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
}
}
Não é bonito, mas a maneira que eu renunciar a firstResponder quando eu não sei o que a resposta é:
Criar um UITextField, seja em IB ou programaticamente. Torná-lo escondido. Vinculá-lo até ao seu código se você fez isso em IB.
Então, quando quiser descartar o teclado, você alternar a resposta ao campo de texto invisível, e resignar-lo imediatamente:
[self.invisibleField becomeFirstResponder];
[self.invisibleField resignFirstResponder];
Para uma versão Swift 3 e 4 de Nevyn é resposta :
UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
Aqui está uma solução que relata o socorrista correto (muitas outras soluções não vai relatar um UIViewController
como o primeiro respondedor, por exemplo), não necessita de looping sobre a hierarquia vista, e não usa APIs privadas.
Ele aproveita método da Apple sendAction: para: desde: forEvent:. , que já sabe como acessar o socorrista ??p>
Nós só precisamos ajustá-lo de 2 maneiras:
- Estender
UIResponder
para que ele possa executar o nosso próprio código no primeiro respondedor. -
UIEvent
subclasse, a fim de devolver o socorrista.
Aqui está o 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
A primeira resposta pode ser qualquer instância do UIResponder classe, de forma que há outras classes que podem ser o primeiro a responder, apesar dos UIViews. Por exemplo UIViewController
também pode ser o primeiro respondedor.
Na esta essência você vai encontrar uma maneira recursiva para obter a primeira resposta por looping através do hierarquia de controladores a partir do RootViewController das janelas do aplicativo.
É possível recuperar em seguida, o socorrista fazendo
- (void)foo
{
// Get the first responder
id firstResponder = [UIResponder firstResponder];
// Do whatever you want
[firstResponder resignFirstResponder];
}
No entanto, se o socorrista não é uma subclasse de UIView ou UIViewController, esta abordagem irá falhar.
Para corrigir esse problema, podemos fazer uma abordagem diferente, criando uma categoria em UIResponder
e realizar alguma magia swizzeling para ser capaz de construir uma matriz de todas as instâncias que vivem desta classe. Então, para obter a primeira resposta que pudermos simples iterate e peça a cada objeto se -isFirstResponder
.
Esta abordagem pode ser encontrada implementado em essa outra essência .
Hope isso ajuda.
Usando Swift e com um objeto UIView
específica isso pode ajudar:
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 colocá-lo em sua UIViewController
e usá-lo como este:
let firstResponder = self.findFirstResponder(inView: self.view)
Tome nota que o resultado é um valor Opcional por isso vai ser nulo no caso de não firstResponser foi encontrado nos dados vistas subviews.
iterar sobre os pontos de vista que poderia ser a primeira resposta eo uso - (BOOL)isFirstResponder
para determinar se eles estão no momento.
Peter Steinberger apenas twittou sobre o UIWindowFirstResponderDidChangeNotification
notificação privada, que você pode observar se você quiser assistir a mudança firstResponder.
Se você só precisa matar o teclado quando as torneiras de usuário em uma área de fundo por que não acrescentar um reconhecedor de gestos e usá-lo para enviar a mensagem [[self view] endEditing:YES]
?
Você pode adicionar o reconhecedor Tap gesto no arquivo xib ou storyboard e conectá-lo a uma ação,
é algo como isto, então, terminou
- (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer{
[[self view] endEditing:YES];
}
Apenas lo caso aqui é a versão Swift da abordagem de incrível 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
}
}
Este é o que eu fiz para encontrar o que UITextField é o firstResponder quando o usuário clicar em Salvar / Cancelar em um 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];
}
}
}
}
}
Isto é o que eu tenho em minha categoria UIViewController. Útil para muitas coisas, incluindo a obtenção de socorrista. Blocos são ótimos!
- (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;
}];
}
Com uma categoria em UIResponder
, é possível pedir legalmente o objeto UIApplication
para dizer-lhe que a primeira resposta é.
Veja este:
existe alguma maneira de pedir uma vista iOS que um de seus filhos tem primeiro estado que responde?
Você pode escolher a seguinte extensão UIView
para obtê-lo (crédito por Daniel) :
extension UIView {
var firstResponder: UIView? {
guard !isFirstResponder else { return self }
return subviews.first(where: {$0.firstResponder != nil })
}
}
Você pode tentar também assim:
- (void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event {
for (id textField in self.view.subviews) {
if ([textField isKindOfClass:[UITextField class]] && [textField isFirstResponder]) {
[textField resignFirstResponder];
}
}
}
Eu não tente, mas parece uma boa solução
A solução de romeo https://stackoverflow.com/a/2799675/661022 é legal, mas eu notei que o código precisa de mais um loop. Eu estava trabalhando com tableViewController. Eu editei o script e, em seguida, eu verifiquei. Tudo funcionou perfeito.
Eu recomendaria tentar este:
- (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 é um bom candidato para a recursividade! Não há necessidade de adicionar uma categoria para UIView.
Uso (de seu controlador de exibição):
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;
}
Você pode chamar api privite como esta, maçã ignorar:
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; SEL sel = NSSelectorFromString(@"firstResponder"); UIView *firstResponder = [keyWindow performSelector:sel];
Gostaria de compartilhado com você minha implementação para achado socorrista em qualquer lugar do UIView. Espero que ajude e desculpe pelo meu Inglês. Graças
+ (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;
}
Versão Swift de @thomas -müller a resposta ??p>
extension UIView {
func firstResponder() -> UIView? {
if self.isFirstResponder() {
return self
}
for subview in self.subviews {
if let firstResponder = subview.firstResponder() {
return firstResponder
}
}
return nil
}
}
Eu tenho uma abordagem um pouco diferente do que a maioria aqui. Ao invés de percorrer a coleção de vistas olhando para aquele que tem um conjunto isFirstResponder
, eu também enviar uma mensagem para nil
, mas eu armazenar o receptor (se houver), em seguida, retornar esse valor para que o chamador recebe a instância real (mais uma vez, se houver).
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, então, renunciar a primeira resposta, se houver, ao fazer isso ...
return UIResponder.first?.resignFirstResponder()
Mas eu agora também pode fazer isso ...
if let firstResponderTextField = UIResponder?.first as? UITextField {
// bla
}
código abaixo trabalho.
- (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;
}
Update: eu estava errado. Você pode realmente usar UIApplication.shared.sendAction(_:to:from:for:)
para chamar o socorrista demonstrado neste link: http://stackoverflow.com/a/14135456/746890.
A maioria das respostas aqui não pode realmente encontrar o atual primeiro respondedor se não está na hierarquia de vista. Por exemplo, AppDelegate
ou UIViewController
subclasses.
Há uma maneira de garantir que você encontrá-lo, mesmo se o primeiro objeto que responde não é um UIView
.
Primeiro vamos implementar uma versão invertida do mesmo, usando a propriedade next
de UIResponder
:
extension UIResponder {
var nextFirstResponder: UIResponder? {
return isFirstResponder ? self : next?.nextFirstResponder
}
}
Com esta propriedade computadorizada, podemos encontrar o primeiro respondedor corrente de baixo para cima, mesmo se não é UIView
. Por exemplo, a partir de um view
ao UIViewController
quem administrá-lo, se o controlador de vista é o primeiro respondedor.
No entanto, ainda precisamos de uma resolução de cima para baixo, um único var
para obter o atual primeiro respondedor.
Primeiro com a hierarquia de exibição:
extension UIView {
var previousFirstResponder: UIResponder? {
return nextFirstResponder ?? subviews.compactMap { $0.previousFirstResponder }.first
}
}
Isto irá procurar os primeiros trás de resposta, e se ele não poderia encontrá-lo, ele iria dizer seus subviews para fazer a mesma coisa (porque next
de seu subexibição não é necessariamente em si). Com isso, podemos encontrá-lo a partir de qualquer ponto de vista, incluindo UIWindow
.
E, finalmente, podemos construir o seguinte:
extension UIResponder {
static var first: UIResponder? {
return UIApplication.shared.windows.compactMap({ $0.previousFirstResponder }).first
}
}
Assim, quando você deseja recuperar o socorrista, você pode ligar no:
let firstResponder = UIResponder.first