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

StackOverflow https://stackoverflow.com/questions/1823317

Вопрос

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

Частный API, включенный в ваше приложение, — firstResponder.

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

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

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

Это было полезно?

Решение

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

Быстрый:

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

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

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

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

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

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

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

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

Для Swift 3 & amp; 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

            }
        }

    }

}

Это то, что есть в моей категории 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 , чтобы получить его (кредит от Daniel) :

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

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

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

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

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

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

Решение от 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]);
                        }
                    }
                }
            }
        }
    }
}

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

Вы можете вызвать конфиденциальный 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;

}

Swift версия @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
    }

}

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

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

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

Обновление: я был не прав. Вы действительно можете использовать 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 { 
extension UIResponder {
    static var first: UIResponder? {
        return UIApplication.shared.windows.compactMap({ 
let firstResponder = UIResponder.first
.previousFirstResponder }).first } }
.previousFirstResponder }.first } }

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

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

<*>

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

<*>
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top