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

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

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

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

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

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

26 ответов

Решение

В одном из моих приложений я часто хочу, чтобы первый респондент подал в отставку, если пользователь нажимает на фон. Для этого я написал категорию на 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`
}

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

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

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

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

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

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

Вот категория, которая позволяет быстро найти первого респондента, позвонив [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 отправляет его первому респонденту.

(Я первоначально опубликовал этот ответ здесь: /questions/19221993/est-li-sposob-sprosit-u-ios-kto-iz-ee-detej-imeet-status-pervogo-respondenta/19222002#19222002)

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

import UIKit

extension UIResponder {
    // Swift 1.2 finally supports static vars!. If you use 1.1 see: 
    // http://stackru.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
    }
}

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

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

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

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

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

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

Вот решение, которое сообщает правильного первого респондента (многие другие решения не будут сообщать 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

Используя 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 не был найден в данных подпредставлениях представлений.

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

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

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

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

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

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

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

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

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

У меня немного другой подход, чем у большинства здесь. Вместо того, чтобы перебирать коллекцию представлений, ища ту, которая имеет 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
}

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

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

В данном случае 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
    }

}

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

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

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

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

Это то, что я сделал, чтобы узнать, что 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];
                }

            }
        }

    }

}

Это хороший кандидат на рекурсию! Нет необходимости добавлять категорию в 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;
}

Это то, что у меня есть в моей категории 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;
 }];
}

С категорией на UIResponder, можно юридически спросить UIApplication объект, чтобы сказать вам, кто первый ответчик.

Видеть это:

Есть ли способ спросить у iOS, кто из ее детей имеет статус первого респондента?

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

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

Обновление: я был неправ. Вы действительно можете использовать UIApplication.shared.sendAction(_:to:from:for:) позвонить первому респонденту, указанному по этой ссылке: /questions/19221993/est-li-sposob-sprosit-u-ios-kto-iz-ee-detej-imeet-status-pervogo-respondenta/19222002#19222002.


Большинство ответов здесь не могут найти текущего первого респондента, если он не находится в иерархии представлений. Например, 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

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

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

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

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

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

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

}

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

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

Я хотел бы поделиться с вами моей реализацией для поиска первого респондента в любом месте 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;

}

Решение от romeo /questions/17715620/poluchit-tekuschij-pervyij-respondent-bez-ispolzovaniya-chastnogo-api/17715641#17715641 - это круто, но я заметил, что коду нужен еще один цикл. Я работал с 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]);
                        }
                    }
                }
            }
        }
    }
}

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

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

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

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 для ваших собственных целей.

Другие вопросы по тегам