Получить текущий первый респондент без использования частного API


340

Я отправил свое приложение чуть более недели назад и получил страшное письмо с отказом сегодня. Это говорит мне, что мое приложение не может быть принято, потому что я использую непубличный API; в частности, это говорит,

Непубличный API, включенный в ваше приложение, это firstResponder.

Теперь вызов API-интерфейса на самом деле является решением, которое я нашел здесь на SO:

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

Как вывести текущего первого респондента на экран? Я ищу способ, которым мое приложение не будет отклонено.


5
Вместо того, чтобы перебирать взгляды, вы можете использовать это поистине гениальное волшебство: stackoverflow.com/a/14135456/746890
Крис Нолет

Ответы:


338

В одном из моих приложений я часто хочу, чтобы первый респондент подал в отставку, если пользователь нажимает на фон. Для этого я написал в UIView категорию, которую я вызываю в UIWindow.

Следующее основано на этом и должно вернуть первого респондента.

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

Пример использования в Swift:

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

278
Почему это лучшее решение, чем просто звонок [self.view endEditing:YES]?
Тим Салливан

69
@ Я не мог, ты бы сделал это. Это, очевидно, гораздо более простой способ уйти в отставку первого респондента. Это не помогает с оригинальными вопросами, которые должны были определить первого респондента.
Томас Мюллер

17
Кроме того, это лучший ответ, потому что вы, возможно, захотите сделать что-то еще с первым респондентом, чем подать в отставку ...
Эрик Б

3
Следует отметить, что это находит только UIViews, а не другие UIResponders, которые также могут быть первым респондентом (например, UIViewController или UIApplication).
Патрик Пийнаппел

10
Почему разница для iOS 7? Разве вы не хотите в этом случае отказаться?
Питер ДеВиз

531

Если вашей конечной целью является просто подать в отставку первого респондента, это должно работать: [self.view endEditing:YES]


122
Для всех вас, говорящих, что это ответ на «вопрос», вы можете взглянуть на реальный вопрос? Вопрос спрашивает, как получить текущего первого респондента. Не как уйти в отставку первым ответчиком.
Джастин Кредибл

2
и где мы должны назвать это self.view endEditing?
user4951

8
Достаточно верно, что это не совсем ответ на вопрос, но, несомненно, это очень полезный ответ. Спасибо!
NovaJoe

6
+1 даже если это не ответ на вопрос. Зачем? Потому что многие приходят сюда с вопросом, на который отвечает этот ответ :) Как минимум 267 (на данный момент) из них ...
Rok Jarc

1
Это не ответ на вопрос.
Сашо

186

Распространенным способом манипулирования первым респондентом является использование целевых действий с нулевым значением. Это способ отправки произвольного сообщения в цепочку респондента (начиная с первого респондента) и продолжения вниз по цепочке, пока кто-то не ответит на сообщение (не реализовал метод, соответствующий селектору).

В случае отклонения клавиатуры это наиболее эффективный способ, который будет работать независимо от того, какое окно или представление является первым респондентом:

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

Это должно быть более эффективным, чем даже [self.view.window endEditing:YES].

(Спасибо BigZaphod за напоминание о концепции)


4
Это так круто. Возможно, самый лучший трюк с Cocoa Touch, который я когда-либо видел на SO. Помогли мне без конца!
mluisbrown

это НАМНОГО лучшее решение, спасибо миллион! это намного лучше, чем решение выше этого, потому что оно работает даже с активным текстовым полем, являющимся частью вспомогательного представления клавиатуры (в этом случае оно не является частью нашей иерархии представлений)
Pizzaiola Gorgonzola

2
Это решение является наилучшим решением. Система уже знает, кто является первым респондентом, и искать его не нужно.
Leftspin

2
Я хочу дать этот ответ более чем один голос. Это чертовски гениально.
Рейд Майн

1
devios1: ответ Якоба - именно то, о чем спрашивает вопрос, но это взлом на вершине системы респондента, а не использование системы респондента, как это было задумано. Это чище и выполняет то, для чего большинство людей приходят к этому вопросу, и в одном месте. (Конечно, вы можете отправить любое сообщение в стиле target-action, а не просто resignFirstResponder).
Невин

98

Вот категория, которая позволяет быстро найти первого респондента по телефону [UIResponder currentFirstResponder]. Просто добавьте следующие два файла в ваш проект:

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

Хитрость в том, что отправка действия на nil отправляет его первому респонденту.

(Я первоначально опубликовал этот ответ здесь: https://stackoverflow.com/a/14135456/322427 )


1
+1 Это самое элегантное решение проблемы (и заданный вопрос), которое я видел. Многоразово и эффективно! Upvote этот ответ!
devios1

3
Браво. Это может быть просто самым красивым и чистым взломом, который я когда-либо видел для Objc ...
Gross

Очень хорошо. Должно работать очень хорошо, чтобы потом иметь возможность спросить первого респондента, кто (если кто-либо) в цепочке будет обрабатывать конкретный метод действия, в котором он должен быть отправлен. Вы можете использовать это, чтобы включить или отключить элементы управления в зависимости от того, могут ли они быть обработаны. Я отвечу на свой вопрос и напишу ваш ответ.
Бенджон


Предупреждение: это не сработает, если вы начнете добавлять несколько окон. Я, к сожалению, пока не могу указать на причину этого.
Хит Бордерс

41

Вот расширение, реализованное в Swift на основе самого превосходного ответа Якоба Эггера:

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

1
Модифицированный ответ для swift1.2, где поддерживаются статические переменные.
nacho4d

Привет, когда я распечатываю результат этого в совершенно новом приложении для одного представления в viewDidAppear (), я получаю ноль. Разве представление не должно быть первым ответчиком?
farhadf

@LinusGeffarth Разве не имеет смысла вызывать статический метод currentвместо first?
Code Commander

Угадайте так, отредактировал это. Я, кажется, упустил важную часть при сокращении имени переменной.
LinusGeffarth

29

Это не красиво, но то, как я оставляю firstResponder, когда я не знаю, что это за ответчик:

Создайте UITextField, либо в IB, либо программно. Сделать это скрытым Свяжите это с вашим кодом, если вы сделали это в IB.

Затем, когда вы хотите отклонить клавиатуру, вы переключаете респондент в невидимое текстовое поле и немедленно отмените его:

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

61
Это одновременно и ужасно, и гениально. Ницца.
Алекс Уэйн

4
Я нахожу это немного опасным - однажды Apple может решить, что сделать скрытый элемент управления первым респондентом не является предполагаемым поведением и «исправить» iOS, чтобы она больше не делала этого, и тогда ваш трюк может перестать работать.
Томас Темпельманн

2
Или вы можете назначить firstResponder для UITextField, который в данный момент видим, а затем вызвать resignFirstResponder для этого конкретного textField. Это устраняет необходимость создавать скрытый вид. Все равно +1.
Swifty McSwifterton

3
Я скорее думаю, что это только ужасно ... это может изменить раскладку клавиатуры (или представление ввода), прежде чем скрывать это
Даниэль

19

Для Swift 3 и 4 версии ответа Невина :

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

10

Вот решение, которое сообщает о правильном первом респонденте (например, многие другие решения не будут сообщать UIViewControllerо первом респонденте), не требует зацикливания в иерархии представления и не использует частные API.

Он использует метод Apple sendAction: to: from: forEvent:, который уже знает, как получить доступ к первому респонденту.

Нам просто нужно настроить его двумя способами:

  • Расширьте, UIResponderчтобы он мог выполнить наш собственный код на первом респонденте.
  • Подкласс UIEventдля того, чтобы вернуть первого респондента.

Вот код:

@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

7

Первым респондентом может быть любой экземпляр класса UIResponder, поэтому существуют другие классы, которые могут быть первым респондентом, несмотря на UIViews. Например, UIViewControllerтакже может быть первым респондентом.

В этом разделе вы найдете рекурсивный способ получения первого респондента, просматривая иерархию контроллеров, начиная с rootViewController окон приложения.

Вы можете получить первого респондента, выполнив

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

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

Однако, если первый респондент не является подклассом UIView или UIViewController, этот подход потерпит неудачу.

Чтобы решить эту проблему, мы можем использовать другой подход, создав категорию UIResponderи выполнив некоторое волшебство, чтобы иметь возможность собрать массив всех живых экземпляров этого класса. Затем, чтобы получить первого респондента, мы можем просто повторить и спросить каждый объект, если -isFirstResponder.

Этот подход может быть найден реализованным в этой другой сути .

Надеюсь, поможет.


7

Используя Swift и с конкретным UIViewобъектом, это может помочь:

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
}

Просто поместите его в свой UIViewControllerи используйте его так:

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

Обратите внимание, что результатом является необязательное значение, поэтому оно будет равно нулю, если firstResponser не был найден в данных подпредставлениях представлений.


5

Переберите представления, которые могут быть первым респондентом, и используйте, - (BOOL)isFirstResponderчтобы определить, являются ли они в данный момент.


4

Питер Штейнбергер только что написал в Твиттере о личном уведомлении UIWindowFirstResponderDidChangeNotification, которое вы можете наблюдать, если хотите увидеть изменение первого респондера.


Его отвергают, потому что использование приватного API, возможно, использование приватного уведомления также может быть плохой идеей ...
Ласло

4

Если вам просто нужно убить клавиатуру, когда пользователь нажимает на фоновую область, почему бы не добавить распознаватель жестов и использовать его для отправки [[self view] endEditing:YES]сообщения?

вы можете добавить распознаватель жестов касания в файл xib или раскадровки и подключить его к действию,

выглядит примерно так потом закончил

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

Это отлично сработало для меня. Я выбрал одно подключение к представлению на Xib, и я могу добавить для этого дополнительные представления, которые будут вызываться в ссылочных коллекциях
розеток

4

В данном случае Swift-версия удивительного подхода Якоба Эггера:

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
    }

}

3

Это то, что я сделал, чтобы узнать, что UITextField является firstResponder, когда пользователь нажимает Сохранить / Отмена в 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];
                }

            }
        }

    }

}

2

Это то, что у меня есть в моей категории UIViewController. Полезно для многих вещей, в том числе получения первого респондента. Блоки отличные!

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

Вы можете выбрать следующее UIViewрасширение, чтобы получить его (кредит от Daniel) :

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

1

Вы также можете попробовать вот так:

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

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

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

Я не пробовал, но это кажется хорошим решением


1

Решение от romeo https://stackoverflow.com/a/2799675/661022 - это круто, но я заметил, что коду нужен еще один цикл. Я работал с tableViewController. Я отредактировал скрипт и затем проверил. Все работало отлично.

Я рекомендовал попробовать это:

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

Спасибо! Вы правы, большинство представленных здесь ответов не являются решением при работе с uitableview. Вы могли бы значительно упростить свой код, даже убрать if if ([textField isKindOfClass:[UITextField class]]), разрешив использование для uitextview, и могли бы вернуть первого респондента.
Frade

1

Это хороший кандидат на рекурсию! Нет необходимости добавлять категорию в UIView.

Использование (с вашей точки зрения контроллера):

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

Код:

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

Вы можете назвать приватный API, как это, яблоко игнорировать:

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

1
но ... пожалуйста, используйте '
respdsToSelector

Сейчас я проверяю «responses toSelector», но все равно получаю предупреждение, это может быть небезопасно. Можно ли как-то избавиться от предупреждения?
ecth

1

Быстрая версия ответа @ 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

У меня немного другой подход, чем у большинства здесь. Вместо того, чтобы перебирать коллекцию представлений в поисках того, который был isFirstResponderустановлен, я тоже отправляю сообщение nil, но сохраняю получателя (если есть), затем возвращаю это значение, чтобы вызывающий получил фактический экземпляр (снова, если есть) ,

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

Затем я могу уйти в отставку первого ответчика, если таковые имеются, сделав это ...

return UIResponder.first?.resignFirstResponder()

Но теперь я тоже могу это сделать ...

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

1

Я хотел бы поделиться с вами моей реализацией для поиска первого респондента в любом месте UIView. Я надеюсь, что это помогает и извините за мой английский. Спасибо

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

0

Код ниже работы.

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

Обновление: я был неправ. Вы действительно можете использовать UIApplication.shared.sendAction(_:to:from:for:)для вызова первого респондента, показанного по этой ссылке: http://stackoverflow.com/a/14135456/746890 .


Большинство ответов здесь не могут найти текущего первого респондента, если он не находится в иерархии представлений. Например, AppDelegateили UIViewControllerподклассы.

Существует способ гарантировать его нахождение, даже если первый объект респондента не является UIView.

Сначала давайте реализуем его обратную версию, используя nextсвойство UIResponder:

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

С помощью этого вычисленного свойства мы можем найти текущего первого респондента снизу вверх, даже если это не так UIView. Например, от оператора viewк тому, UIViewControllerкто им управляет, если контроллер представления является первым респондентом.

Однако нам все еще нужно разрешение сверху вниз, единственное, varчтобы получить текущий первый респондент.

Сначала с иерархией представления:

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

Он будет искать первого респондента в обратном направлении, и если он не сможет его найти, он скажет своим подпредставлениям сделать то же самое (потому что его подпредставление nextне обязательно само по себе). При этом мы можем найти его с любой точки зрения, в том числе UIWindow.

И, наконец, мы можем построить это:

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

Поэтому, когда вы хотите получить первого респондента, вы можете позвонить:

let firstResponder = UIResponder.first

0

Самый простой способ найти первого респондента:

func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool

Реализация по умолчанию отправляет метод действия указанному целевому объекту или, если цель не указана, первому респонденту.

Следующий шаг:

extension UIResponder
{
    private weak static var first: UIResponder? = nil

    @objc
    private func firstResponderWhereYouAre(sender: AnyObject)
    {
        UIResponder.first = self
    }

    static var actualFirst: UIResponder?
    {
        UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
        return UIResponder.first
    }
}

Использование: Просто получите UIResponder.actualFirstдля своих собственных целей.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.