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

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

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

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

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

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

Ответ 1

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

Ответ 2

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

Ответ 3

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

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

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

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

(Благодаря BigZaphod, чтобы напомнить мне о концепции)

Ответ 4

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

(Я изначально опубликовал этот ответ здесь: fooobar.com/questions/18028/...)

Ответ 5

Вот расширение, реализованное в 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
    }
}

Ответ 6

Это некрасиво, но так, как я ухожу в отставку первогоResponder, когда я не знаю, что это за ответчик:

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

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

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

Вы можете получить первый ответчик, выполнив

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

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

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

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

Этот подход можно найти в этот другой метод.

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

Ответ 10

Используя 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.

Ответ 11

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

Ответ 12

Peter Steinberger просто в твиттере о частном уведомлении UIWindowFirstResponderDidChangeNotification, которое вы можете наблюдать, если хотите посмотреть, как изменилось первое изменение.

Ответ 13

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

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

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

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

Ответ 14

В этом случае речь идет о 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
    }

}

Ответ 15

Это то, что я сделал, чтобы найти, что UITextField является первымResponder, когда пользователь щелкает Save/Cancel в 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];
                }

            }
        }

    }

}

Ответ 16

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

Ответ 18

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

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

Ответ 19

Вы также можете попробовать следующее:

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

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

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

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

Ответ 20

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

Ответ 21

Решение от romeo fooobar.com/questions/17897/... классно, но я заметил, что для этого кода нужен еще один цикл. Я работал с tableViewController. Я редактировал script, а затем я проверил. Все работало отлично.

Я рекомендовал попробовать следующее:

- (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 superview: %@", [theTextField superview]);
                        }
                    }
                }
            }
        }
    }
}

Ответ 22

вы можете вызвать privite api вот так, apple ignore:

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

Ответ 23

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

}

Ответ 24

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

}

Ответ 25

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

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

Ответ 26

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

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

Ответ 27

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


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

Ответ 28

В Swift 5 в классе UIResponder есть "текущий" необязательный атрибут, который можно использовать для получения активного в данный момент респондента. Выглядит так:

UIResponder.current?.resignFirstResponder()

Ответ 29

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

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