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


339

我在一周前提交了我的应用程序,今天收到了可怕的拒绝电子邮件。它告诉我我的应用无法接受,因为我使用的是非公开API;具体说,

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

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

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

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


5
无需遍历视图,您可以使用真正出色的魔术:stackoverflow.com/a/14135456/746890
Chris Nolet 2014年

Answers:


338

在我的一个应用程序中,如果用户在后台点击,我经常希望第一响应者辞职。为此,我在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`
}

278
为什么这比简单地调用更好的解决方案[self.view endEditing:YES]
蒂姆·沙利文

69
@蒂姆,我不是你能做到的。显然,这是辞退第一响应者的简单得多的方法。但是,这对于确定最初的响应者并没有帮助。
ThomasMüller,2010年

17
此外,这是一个更好的答案,因为您可能想对第一响应者做其他事情而不是辞职……
Erik B

3
应当注意,这仅找到UIViews,而不是其他也可能是第一响应者的UIResponder(例如,UIViewController或UIApplication)。
Patrick Pijnappel 2014年

10
为什么iOS 7与众不同?在这种情况下,您也不想递归吗?
Peter DeWeese 2014年

531

如果您的最终目的只是辞职第一响应者,那么这应该可行: [self.view endEditing:YES]


122
你们所有人都说这是“问题”的答案,您可以看看实际的问题吗?问题询问如何获取当前的第一响应者。不是如何辞职第一响应者。
贾斯汀·克雷迪布

2
以及我们应该在何处将此self.view endEditing命名?
user4951 '11

8
确实如此,以至于这不能完全回答问题,但显然这是一个非常有用的答案。谢谢!
NovaJoe 2012年

6
+1,即使这不是问题的答案。为什么?因为许多人想到这里来回答这个问题:)至少有267名(目前)……
Rok Jarc

1
这没有回答问题。
Sasho 2014年

186

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

对于关闭键盘的情况,这是最有效的方法,无论哪个窗口或视图是第一响应者,该方法都将起作用:

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

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

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


4
太酷了 我可能在SO上看到过的最整洁的可可触摸把戏。帮助我没有尽头!
mluisbrown

这是BY FAR最好的解决方案,谢谢!这比上面的解决方案要好得多,因为它即使在活动文本字段中也可以工作,是键盘辅助视图的一部分(在这种情况下,它不属于我们的视图层次结构)
Pizzaiola Gorgonzola

2
此解决方案是最佳实践解决方案。系统已经知道谁是第一响应者,因此无需找到它。
左旋2014年

2
我想给这个答案一个以上的投票。这是个骗人的天才。
Reid Main

1
devios1:Jakob的答案正是问题的根源,但这是对响应者系统的攻击,而不是按照设计的方式使用响应者系统。这是更清洁的方法,并且可以一口气完成大多数人要解决的问题。(当然,您可以发送任何目标动作样式的消息,而不仅仅是resignFirstResponder)。
nevyn

97

这是一个类别,可让您通过致电快速找到第一响应者[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


1
+1这是我所见过的最完美的解决方案(以及所提出的实际问题)。可重复使用,高效!支持这个答案!
devios1 2014年

3
太棒了 这可能只是我见过的Objc所见过的最美丽,最干净的骇客……
Aviel Gross 2014年

非常好。这样应该可以很好地工作,以便能够询问第一响应者链中的谁(如果有的话)将处理特定的操作方法。您可以使用此选项根据是否可以处理来启用或禁用控件。我将回答我自己的问题,并参考您的回答。
Benjohn 2015年


警告:如果您开始添加多个窗口,则此操作无效。不幸的是,我无法指出除此以外的原因。
Heath Borders

40

这是基于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
    }
}

斯威夫特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的修改答案,其中支持静态var。
nacho4d

嗨,当我在viewDidAppear()的全新单视图应用程序上打印出此结果时,得到的结果为nil。该视图不应该是第一响应者吗?
farhadf

@LinusGeffarth current而不是调用static方法更有意义first吗?
代码指挥官

猜猜吧,对其进行了编辑。缩写变量名称时,我似乎已删除了重要部分。
LinusGeffarth

29

这不是很漂亮,但是当我不知道响应者是什么时,我辞退firstResponder的方式:

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

然后,当您想要关闭键盘时,将响应器切换到不可见的文本字段,然后立即将其辞职:

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

61
这既是恐怖的又是天才。真好
亚历

4
我发现有些危险-苹果可能有一天会决定使隐藏控件成为第一响应者不是故意的行为,并且“修复” iOS不再这样做,然后您的把戏可能会停止。
Thomas Tempelmann 2012年

2
或者,您可以将firstResponder分配给当前可见的UITextField,然后在该特定textField上调用resignFirstResponder。这样就无需创建隐藏视图。都一样,+ 1。
Swifty McSwifterton

3
我宁愿认为这只是可怕的事情……它可能会在隐藏键盘之前更改键盘布局(或输入视图)
Daniel

19

对于nevyn的答案的Swift 3和4版本:

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

10

这是一个报告正确的第一响应者的解决方案(例如,许多其他解决方案都不会报告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

7

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

本要点中,您将找到一种通过从应用程序窗口的rootViewController开始遍历控制器的层次结构来获取第一响应者的递归方法。

您可以通过执行以下操作来检索第一响应者

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

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

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

为了解决这个问题,我们可以通过在其上创建一个类别UIResponder并执行一些魔术来构建该类的所有活动实例的数组,从而采取不同的方法。然后,要获得第一响应者,我们可以简单地迭代并询问每个对象是否为-isFirstResponder

可以在其他要点中实现该方法。

希望能帮助到你。


7

使用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)

请注意,结果是一个Optional值,因此如果在给定的view子视图中未找到firstResponser,则它将为nil。


5

遍历可能是第一响应者的视图,并用于- (BOOL)isFirstResponder确定它们当前是否为当前视图。


4

彼得·斯坦伯格(Peter Steinberger)刚刚发布了有关私人通知的推文UIWindowFirstResponderDidChangeNotification,您可以观察是否要观看firstResponder的更改。


他被拒绝了,因为使用私有api,也许使用私有通知也可能不是一个好主意……
Laszlo

4

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

您可以在xib或情节提要文件中添加点击手势识别器,并将其连接到动作,

看起来像这样,然后完成

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

这对我来说很棒。我选择了一个与Xib上的视图的连接,并且可以为此添加其他视图以在“引用插座集合”中调用
James Perih 2013年

4

这只是案例,它是Jakob Egger出色方法的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
    }

}

3

这是我在用户单击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];
                }

            }
        }

    }

}

2

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


2

您可以选择以下UIView扩展名进行获取(Daniel提供)

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

1

您也可以这样尝试:

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

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

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

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


1

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

谢谢!您是正确的,使用uitableview时,此处提供的大多数答案不是解决方案。您可以简化很多代码,甚至可以取出if if ([textField isKindOfClass:[UITextField class]]),允许将其用于uitextview,并且可以返回firstresponder。
Frade 2015年

1

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

1

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

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

1
但是...请使用'respondsToSelector'检查
ibcker 2014年

我现在确实检查了“ respondsToSelector”,但仍然收到警告,这可能是不安全的。我可以摆脱警告吗?
2015年

1

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
    }

}

1

我的方法与这里的大多数方法略有不同。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
}

1

我想与您分享我的实现,以在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;
}

0

下面的代码工作。

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

0

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


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

有一种方法可以保证即使第一个响应方对象不是 UIView

首先,使用的next属性实现其反向版本UIResponder

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

0

查找急救人员的最简单方法:

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仅供您自己使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.