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

我在一个多星期前提交了应用,今天收到了可怕的拒绝邮件。它告诉我,我的应用程序不能被接受,因为我使用的是非公共API;具体来说,它说,

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

现在,违规的API调用实际上是一个解决方案,我发现这里的SO:

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

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

174721 次浏览

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

在我的一个应用程序中,如果用户在后台点击,我经常希望第一响应器辞职。为了这个目的,我在UIView上写了一个category,我在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`
}

这是我所做的,当用户单击ModalViewController中的Save/Cancel时,找到什么UITextField是第一个responder:

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


}
}


}


}

如果你的最终目标只是辞职第一个响应者,这应该工作:[self.view endEditing:YES]

这不是很漂亮,但是当我不知道responder是什么时,我辞去firstResponder的方式:

创建一个UITextField,无论是在IB或编程。把它隐藏起来。如果你是用IB写的,就把它链接到你的代码上。

然后,当你想要解散键盘时,你将响应器切换到不可见的文本字段,并立即辞职:

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

你也可以这样尝试:

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


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


if ([textField isKindOfClass:[UITextField class]] && [textField isFirstResponder]) {
[textField 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视图,它的哪个子具有第一响应器状态?< / >

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

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

你可以通过这样来检索第一个响应器

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


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

然而,如果第一响应器不是UIView或UIViewController的子类,这种方法将失败。

为了解决这个问题,我们可以采用不同的方法,在UIResponder上创建一个类别,并执行一些神奇的旋转,以便能够构建一个包含该类所有活实例的数组。然后,为了获得第一个responder,我们可以简单地迭代并询问每个对象是否-isFirstResponder

此方法可以在另一个要点中找到实现。

希望能有所帮助。

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

对于取消键盘的情况,这是最有效的方法,无论哪个窗口或视图是第一响应器:

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

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

(感谢BigZaphod提醒我的概念)

Peter Steinberger刚刚在推特上关于私有通知UIWindowFirstResponderDidChangeNotification,如果你想观察firstResponder的变化,你可以观察到。

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

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

看起来像这样,然后完成

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

这是递归的一个很好的候选!不需要添加一个类别到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;
}

下面是一个类别,它允许你通过调用[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)

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

下面是一个报告正确的第一响应器的解决方案(例如,许多其他解决方案不会报告UIViewController作为第一响应器),不需要遍历视图层次结构,并且不使用私有api。

它利用了Apple的方法sendAction::: forEvent:,该方法已经知道如何访问第一个响应器。

我们只需要在2个方面进行调整:

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

你可以像这样调用私有API,苹果忽略:

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

下面是一个基于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
}
}

使用斯威夫特特定的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。

我想和你们分享我的实现,在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;
}

@thomas-muller的响应的Swift版本

extension UIView {


func firstResponder() -> UIView? {
if self.isFirstResponder() {
return self
}


for subview in self.subviews {
if let firstResponder = subview.firstResponder() {
return firstResponder
}
}


return nil
}


}

这里是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
}


}

下面的代码可以工作。

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

为了一个Swift 3 &4版本的< >强nevyn < /强大的>答案:

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

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


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

有一种方法可以保证你找到它,即使第一个responder对象不是UIView

首先让我们使用UIRespondernext属性来实现它的反转版本:

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

使用这个computed属性,我们可以从下到上找到当前的第一响应器,即使它不是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

我不是遍历视图集合寻找设置了isFirstResponder的视图,而是也向nil发送消息,但我存储了消息的接收器,以便我可以返回它并对它做任何我想做的事情。

此外,我将在调用本身的defer语句中保存找到的responder的optional归零。这确保在调用结束时没有引用保留——即使是弱引用。

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 and place it in the _foundFirstResponder variable.
// As such, the variable will contain the current first responder (if any) immediately after this line executes
UIApplication.shared.sendAction(#selector(UIResponder.storeFirstResponder(_:)), to: nil, from: nil, for: nil)


// The following 'defer' statement runs *after* this getter returns,
// thus releasing any strong reference held by the variable immediately thereafter
defer {
_foundFirstResponder = nil
}


// Return the found first-responder (if any) back to the caller
return _foundFirstResponder
}


// Make sure to mark this with '@objc' since it has to be reachable as a selector for `sendAction`
@objc func storeFirstResponder(_ sender: AnyObject) {


// Capture the recipient of this message (self), which is the first responder
_foundFirstResponder = self
}
}

有了上面的,我可以通过简单地这样做辞去第一响应者…

UIResponder.first?.resignFirstResponder()

但由于我的API实际上交还了第一个responder是什么,我可以对它做任何我想做的事情。

下面是一个示例,它检查当前的第一响应器是否是具有helpMessage属性集的UITextField,如果是,则在控件旁边的帮助气泡中显示它。我们通过屏幕上的“Quick Help”按钮来调用它。

func showQuickHelp(){


if let textField = UIResponder?.first as? UITextField,
let helpMessage = textField.helpMessage {
    

textField.showHelpBubble(with:helpMessage)
}
}

对上述功能的支持在UITextField上的扩展中定义,如下所示:

extension UITextField {
var helpMessage:String? { ... }
func showHelpBubble(with message:String) { ... }
}

现在要支持这个功能,我们所要做的就是决定哪些文本字段有帮助消息,UI会为我们处理其余的事情。

你可以选择下面的UIView扩展来获得它(丹尼尔提供):

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

找到第一响应者的最简单方法:

func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool
默认实现将action方法分派给给定对象 目标对象,如果没有指定目标,则发送给第一个响应器

下一步:

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
}
}
< p >用法: