簡體   English   中英

在不使用私有 API 的情況下獲取當前的第一響應者

[英]Get the current first responder without using a private API

我在一個多星期前提交了我的應用程序,今天收到了可怕的拒絕電子郵件。 它告訴我我的應用程序無法被接受,因為我使用的是非公共 API; 具體來說,它說,

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

現在,有問題的 API 調用實際上是我在 SO 上找到的解決方案:

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

如何在屏幕上顯示當前的第一響應者? 我正在尋找一種不會讓我的應用程序被拒絕的方法。

如果你的最終目標只是讓第一響應者辭職,這應該有效: [self.view endEditing:YES]

在我的一個應用程序中,如果用戶點擊背景,我經常希望第一響應者辭職。 為此,我在 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`
}

操縱第一響應者的常見方法是使用 nil 目標操作。 這是一種將任意消息發送到響應者鏈(從第一個響應者開始)並繼續沿鏈向下直到有人響應該消息(已實現與選擇器匹配的方法)的方式。

對於關閉鍵盤的情況,無論哪個窗口或視圖是第一響應者,這是最有效的方法:

[[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

這里的技巧是將一個動作發送到 nil 將它發送給第一響應者。

(我最初在這里發布了這個答案: https : //stackoverflow.com/a/14135456/322427

這是基於 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
    }
}

這不是很漂亮,但是當我不知道響應者是什么時,我退出 firstResponder 的方式:

在 IB 中或以編程方式創建一個 UITextField。 使其隱藏。 如果您是在 IB 中制作的,則將其鏈接到您的代碼。

然后,當你想關閉鍵盤時,你將響應者切換到不可見的文本字段,並立即辭職:

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

對於 Swift 3 & 4 版本的nevyn答案:

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上創建一個類別來做一個不同的方法,並執行一些神奇的 swizzeling 來構建一個包含這個類的所有實例的數組。 然后,為了獲得第一響應者,我們可以簡單地迭代並詢問每個對象是否為-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)

請注意,結果是一個Optional 值,因此如果在給定的視圖子視圖層次結構中找不到 firstResponder,它將為零。

迭代可能是第一響應者的視圖並使用- (BOOL)isFirstResponder來確定它們當前是否是。

Peter Steinberger 剛剛發布了關於私有通知UIWindowFirstResponderDidChangeNotification推文,如果你想觀看 firstResponder 的變化,你可以觀察它。

如果您只需要在用戶點擊背景區域時關閉鍵盤,為什么不添加手勢識別器並使用它來發送[[self view] endEditing:YES]消息?

您可以在 xib 或故事板文件中添加 Tap 手勢識別器並將其連接到一個動作,

看起來像這樣然后完成

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

只是這里的情況是令人敬畏的 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
    }

}

這就是我在用戶單擊 ModalViewController 中的保存/取消時找到 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];
                }

            }
        }

    }

}

我的方法與大多數人略有不同。 我沒有遍歷視圖集合以查找設置了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 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
    }

    @objc func storeFirstResponder(_ sender: AnyObject) {

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

有了上述內容,我可以通過簡單地這樣做來辭去第一響應者的職務......

UIResponder.first?.resignFirstResponder()

但是由於我的 API 實際上交還了第一響應者是什么,我可以用它做任何我想做的事情。

這是一個示例,用於檢查當前的第一響應者是否是設置了helpMessage屬性的UITextField ,如果是,則將其顯示在控件旁邊的幫助氣泡中。 我們通過屏幕上的“快速幫助”按鈕調用它。

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 會為我們處理其余的工作。

使用UIResponder上的類別,可以合法地要求UIApplication對象告訴您誰是第一響應者。

看到這個:

有什么方法可以詢問 iOS 視圖的哪個孩子具有第一響應者狀態?

您可以選擇以下UIView擴展來獲取它(Daniel 提供)

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

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

這是遞歸的好候選! 無需向 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;
}

你可以像這樣調用privite 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;
}

@thomas-müller回應的 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
    }

}

你也可以這樣試試:

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

下面的代碼工作。

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


如果不在視圖層次結構中,這里的大多數答案都無法真正找到當前的第一響應者。 例如, AppDelegateUIViewController子類。

即使第一響應者對象不是UIView ,也有一種方法可以保證您找到它。

首先讓我們使用UIRespondernext屬性實現它的反向版本:

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

有了這個計算屬性,我們可以從下到上找到當前的第一響應者,即使它不是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

找到第一響應者的最簡單方法:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM