不使用私有API获取当前的第一响应者

365

我在一周多前提交了我的应用程序,今天收到了令人不安的拒绝邮件。它告诉我我的应用程序不能被接受,因为我正在使用一个非公共API;具体地说,它说:

您的应用程序中包含的非公共API是firstResponder。

现在,冒犯的API调用实际上是我在SO上找到的解决方案:

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

如何获取当前屏幕上的第一响应者?我正在寻找一种不会导致我的应用被拒绝的方法。


6
与其遍历视图,你可以使用这个真正精彩的神奇方法:https://dev59.com/zm445IYBdhLWcg3wH2rH#14135456。 - Chris Nolet
28个回答

535
如果您的最终目的只是取消第一个响应者,那么可以使用以下代码:[self.view endEditing:YES]

136
你们所有认为这是“问题”的答案的人,能否看一下实际的问题?问题要求获取当前的第一响应者,而不是辞去第一响应者的职务。 - Justin Kredible
2
我们应该在哪里调用self.view endEditing呢? - user4951
8
确实,这并不完全回答问题,但显然这是一个非常有用的答案。谢谢! - NovaJoe
6
即使它不是问题的答案,也要点赞加一。为什么?因为很多人来这里都有一个问题,而这个回答可以解答他们的问题 :) 至少有267个人(目前为止)...... - Rok Jarc
3
这并没有回答问题。 - Sasho
显示剩余4条评论

363

在我的一个应用程序中,如果用户在背景上点击,我经常希望第一响应者放弃响应。为此,我编写了一个在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`
}

281
为什么这是比简单调用 [self.view endEditing:YES] 更好的解决方案? - Tim Sullivan
73
@Tim,我不知道你能做到这一点。这显然是一种更简单的辞去第一响应者的方法。但这并没有回答最初的问题,即如何确定第一响应者身份。 - Thomas Müller
21
这是一个更好的答案,因为你可能想对第一个响应者做一些其他的事情,而不是仅仅辞职它... - Erik B
6
需要注意的是,此方法仅查找UIView,而不是其他可能是第一响应者的UIResponder(例如UIViewController或UIApplication)。 - Patrick Pijnappel
10
为什么在iOS 7上会有差异?你不也希望在那种情况下进行递归吗? - Peter DeWeese
显示剩余12条评论

197

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

对于关闭键盘的情况,这是最有效的方法,不管哪个窗口或视图是第一个响应者:

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

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

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


4
太酷了!可能是我在 Stack Overflow 上看到的最好的 Cocoa Touch 技巧。对我帮助很大! - mluisbrown
这绝对是最好的解决方案,非常感谢!这比上面的解决方案要好得多,因为它甚至可以在活动文本字段是键盘附件视图的一部分时工作(在这种情况下,它不是我们视图层次结构的一部分)。 - Pizzaiola Gorgonzola
2
这个解决方案是最佳实践的解决方案。系统已经知道第一响应者是谁,无需再寻找。 - leftspin
3
我想给这个答案点赞不止一次。这太聪明了。 - Reid Main
1
devios1:Jakob的回答正是问题所要求的,但它是在响应器系统之上的一个hack,而不是按照响应器系统设计的方式使用。这种方法更加简洁,实现了大多数人来到这个问题的目的,而且只需要一行代码。(当然,您可以发送任何目标-操作样式的消息,而不仅仅是resignFirstResponder)。 - nevyn
显示剩余6条评论

109

这里有一个分类可以通过调用[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://dev59.com/zm445IYBdhLWcg3wH2rH#14135456


1
这是我见过的最优雅、最实际解决问题(也是所问的问题)的方法。可重用且高效!请点赞这个回答! - devios1
3
太棒了。这可能是我见过的针对Objc最美丽、最干净的hack。 - Aviel Gross
1
警告:如果您开始添加多个窗口,则此方法将无法正常工作。不幸的是,我目前还无法确定原因。 - Heath Borders
这是一个真正的答案,不像其他几个超级赞的答案实际上并没有回答问题。而被接受的答案则非常低效。 - mxcl
6
在iOS 14中,苹果公司禁止在传递给-[UIApplication sendAction:to:from:forEvent:]的选择器中使用方法名findFirstResponder。看起来他们注意到了这个问题。但是你可以使用一个不同的方法名来代替它。 - Nick
显示剩余5条评论

51
这是一个基于Jakob Egger非常好的答案实现的Swift扩展:
import UIKit

extension UIResponder {
    // Swift 1.2 finally supports static vars!. If you use 1.1 see: 
    // https://dev59.com/eWAg5IYBdhLWcg3wDHfS#24924535
    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

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()中打印出这个全新的单视图应用程序的结果时,我得到了nil。难道视图不应该是第一响应者吗? - farhadf
@LinusGeffarth 静态方法叫做 current 而不是 first,这难道不更合理吗? - Code Commander
猜想是这样,我已经编辑过了。当我缩写变量名时,似乎遗漏了重要部分。 - LinusGeffarth
不适用于Swift 5.4,我已经尝试了一个textfield和一个textview作为同一视图的子项,但当前值始终为空。 - Alessandro Ornano

29

这不是很漂亮,但当我不知道响应者是什么时,我解除第一响应者的方式如下:

创建一个UITextField,可以在Interface Builder(IB)中或通过编程方式创建。将它隐藏起来,并且如果你在IB中创建了它,请将其链接到你的代码。

然后,当你想要关闭键盘时,将响应者切换到不可见的文本字段,并立即解除它:

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

63
这既恐怖又巧妙。不错。 - Alex Wayne
4
我觉得这有点危险——苹果可能会决定让隐藏的控件不再成为第一响应者,而把它作为第一响应者可能被视为意外行为,并且“修复”iOS使其不再起作用,那么你的技巧可能就无法使用了。 - Thomas Tempelmann
2
或者您可以将firstResponder分配给当前可见的UITextField,然后在该特定textField上调用resignFirstResponder。这样可以避免创建隐藏视图的需要。同样的,+1。 - Swifty McSwifterton
3
我认为这只是可怕的......它可能会在隐藏键盘之前更改键盘布局(或输入视图)。 - Daniel

22

以下是 nevyn 的回答的Swift 3和4版本:

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

11

以下是一个解决方案,它可以报告正确的第一响应者(许多其他解决方案将不会报告UIViewController作为第一响应者,这里会),不需要循环遍历视图层次结构,并且不使用私有API。

它利用了苹果的方法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
使用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)

请注意,结果是一个可选值,所以如果在给定的视图子视图层次结构中找不到firstResponder,则为nil。


8
首先响应者可以是任何UIResponder类的实例,因此除了UIView之外,可能还有其他类是第一个响应者。例如UIViewController也可能是第一个响应者。
this gist中,您将找到一种递归方式来循环遍历控制器层次结构,从应用程序窗口的rootViewController开始获取第一个响应者。
然后,您可以通过执行以下操作来检索第一个响应者:
- (void)foo
{
    // Get the first responder
    id firstResponder = [UIResponder firstResponder];

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

然而,如果第一个响应者不是UIView或UIViewController的子类,则此方法将失败。
为了解决这个问题,我们可以采用不同的方法,通过在UIResponder上创建一个类别并执行一些神奇的交换来构建该类的所有实例的数组。然后,要获取第一个响应者,我们只需迭代并询问每个对象是否-isFirstResponder。
可以在this other gist中找到此方法的实现。
希望能有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接