提问



我在一周前提交了我的应用程序,今天收到了可怕的拒绝电子邮件。它告诉我,我的应用程序无法被接受,因为我使用非公共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`
}

其它参考1


如果你的最终目的只是让第一响应者辞职,这应该有效:[self.view endEditing:YES]

其它参考2


操纵第一响应者的常用方法是使用零目标操作。这是一种向响应者链发送任意消息的方式(从第一个响应者开始),然后继续向下直到有人响应消息(已经实现了与选择器匹配的方法)。


对于解除键盘的情况,无论哪个窗口或视图是第一响应者,这都是最有效的方式:


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


这应该比甚至[self.view.window endEditing:YES]更有效。


(感谢BigZaphod提醒我这个概念)[61]

其它参考3


这是一个类别,允许您通过调用[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)

其它参考4


这是一个基于Jakob Egger最优秀答案的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
    }
}

其它参考5


它不漂亮,但是当我不知道响应者是什么时,我辞职第一个响应者的方式:


在IB中或以编程方式创建UITextField。把它隐藏起来。如果您在IB中创建它,请将其链接到您的代码。


然后,当您想要关闭键盘时,将响应者切换到不可见的文本字段,并立即将其重新签名:


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

其它参考6


这里报告正确的第一响应者的解决方案(许多其他解决方案不会报告UIViewController作为第一响应者,例如),不需要在视图层次结构上循环,并且不使用私有API 。


它利用Apple的方法sendAction:to:from:forEvent:,它已经知道如何访问第一个响应者。[63]


我们只需要通过两种方式进行调整:



  • 扩展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


对于Swift 3版本的 nevyn答案:


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

其它参考8


第一个响应者可以是类UIResponder的任何实例,因此尽管有UIViews,还有其他类可能是第一个响应者。例如UIViewController也可能是第一个响应者。


在这个要点中,您将找到一种递归方式,通过从应用程序窗口的rootViewController开始循环遍历控制器层次结构来获取第一个响应者。[64]


您可以通过执行来检索第一个响应者


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

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


但是,如果第一个响应者不是UIView或UIViewController的子类,则此方法将失败。


为了解决这个问题,我们可以通过在UIResponder上创建一个类别来执行不同的方法,并执行一些魔法调整,以便能够构建此类的所有生存实例的数组。然后,为了得到第一个响应者,我们可以简单地迭代并询问每个对象是否-isFirstResponder


这种方法可以在另一个要点中找到。[65]


希望能帮助到你。

其它参考9


使用 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,它将为零。

其它参考10


迭代可能是第一响应者的视图并使用- (BOOL)isFirstResponder来确定它们当前是否是。

其它参考11


Peter Steinberger刚刚发布了有关私人通知UIWindowFirstResponderDidChangeNotification的推文,如果您想观看第一个响应者更改,您可以观察到[[。[66]

其它参考12


如果您只需要在用户点击背景区域时杀死键盘,为什么不添加手势识别器并使用它来发送[**self view] endEditing:YES]消息?


您可以在xib或storyboard文件中添加Tap手势识别器并将其连接到动作,


看起来像这样然后完成


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

其它参考13


这是我在用户单击ModalViewController中的Save/Cancel时找到UITextField是firstResponder的方法:


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

            }
        }

    }

}

其它参考14


这就是Swift版本的令人敬畏的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
    }

}

其它参考15


这是我在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;
 }];
}

其它参考16


通过UIResponder上的类别,可以合法地要求UIApplication对象告诉您第一响应者是谁。


看到这个:


有没有办法让iOS查看哪个子有第一响应者身份?

其它参考17


您也可以尝试这样:


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

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

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


我没有尝试过,但这似乎是一个很好的解决方案

其它参考18


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

其它参考19


这是递归的好选择!无需向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;
}

其它参考20


你可以像这样调用privite api,苹果忽略:



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


其它参考21


我想与你分享我在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;


}

其它参考22


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
    }

}

其它参考23


代码如下工作。


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

其它参考24


更新:我错了。您确实可以使用UIApplication.shared.sendAction(_:to:from:for:)来呼叫此链接中演示的第一个响应者:http://stackoverflow.com/a/14135456/746890。





如果它不在视图层次结构中,那么这里的大多数答案都不能真正找到当前的第一响应者。例如,AppDelegateUIViewController子类。


即使第一个响应者对象不是UIView,也可以保证找到它。


首先,使用UIRespondernext属性实现它的反转版本:


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


有了这个计算属性,我们可以从下到上找到当前的第一响应者,即使它不是UIView。例如,从viewUIViewController谁管理它,如果视图控制器是第一响应者。


但是,我们仍然需要一个自上而下的分辨率,一个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