web-dev-qa-db-ja.com

プライベートAPIを使用せずに現在のファーストレスポンダを取得する

私は1週間以上前に私のアプリを提出し、今日恐ろしい拒絶メールを受け取りました。非公開のAPIを使用しているため、アプリを承認できないことがわかります。具体的には、

アプリケーションに含まれている非公開のAPIはfirstResponderです。

さて、問題のあるAPI呼び出しは、実際にここでSOで見つけた解決策です。

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

現在のファーストレスポンダを画面に表示する方法アプリが拒否されないような方法を探しています。

328
Justin Kredible

私のアプリケーションの1つでは、ユーザーがバックグラウンドをタップした場合、最初のレスポンダに辞任させることがよくあります。この目的のために私は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`
}
325
Thomas Müller

あなたの究極の目的がファーストレスポンダを辞任することだけであれば、これでうまくいくはずです:[self.view endEditing:YES]

529
cdyson37

ファーストレスポンダを操作する一般的な方法は、ターゲットを絞ったアクションを使用しないことです。これは(最初のレスポンダから始めて)レスポンダチェーンに任意のメッセージを送信し、誰かがそのメッセージに応答するまで(セレクタに一致するメソッドを実装して)チェーンを続けていく方法です。

キーボードを消す場合、これはどのウィンドウまたはビューが最初のレスポンダであるかに関係なく動作する最も効果的な方法です。

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

これは[self.view.window endEditing:YES]よりも効果的です。

(このコンセプトを思い出してくれてありがとう BigZaphod

181
nevyn

これは[UIResponder currentFirstResponder]を呼び出すことによってあなたがすぐに最初の応答者を見つけることを可能にするカテゴリです。次の2つのファイルをプロジェクトに追加するだけです。

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

91
Jakob Egger

これは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
    }
}
35
Code Commander

それはきれいではありませんが、レスポンダが何であるかわからないときにfirstResponderを辞任する方法です。

IBまたはプログラムでUITextFieldを作成します。隠してください。 IBで作成した場合は、それを自分のコードにリンクします。

その後、キーボードを消したいときは、レスポンダを非表示のテキストフィールドに切り替えて、すぐに辞任します。

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

nevynの 回答のSwift 3および4バージョンの場合:

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

正しいファーストレスポンダを報告するソリューションがあります(他の多くのソリューションでは、たとえば、ファーストレスポンダとしてUIViewControllerが報告されることはありません)。ビュー階層をループする必要はなく、プライベートAPIも使用されません。

これはAppleのメソッド sendAction:to:from: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
10
Senseful

ファーストレスポンダは、UIResponderクラスの任意のインスタンスにすることができます。そのため、UIViewsにもかかわらず、ファーストレスポンダになる可能性があるクラスは他にもあります。例えばUIViewControllerも最初の応答者かもしれません。

this Gist では、アプリケーションのウィンドウのrootViewControllerから始めてコントローラの階層をループすることによって、最初のレスポンダを取得するための再帰的な方法を見つけるでしょう。

そうすれば、最初のレスポンダを取得できます。

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

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

ただし、最初のレスポンダがUIViewまたはUIViewControllerのサブクラスではない場合、この方法は失敗します。

この問題を解決するには、UIResponder上にカテゴリを作成し、このクラスのすべての生きているインスタンスの配列を構築できるようにするために魔法のスウィズルを実行することで別のアプローチをとることができます。次に、最初のレスポンダを取得するために、単純に反復して各オブジェクトに-isFirstResponderかどうかを尋ねます。

このアプローチは この他の要旨 に実装されています。

それが役に立てば幸い。

7
vilanovi

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が見つからなかった場合は、ゼロになります。

6
Dschee

最初のレスポンダとなる可能性のあるビューを繰り返し、現在のビューかどうかを判断するために- (BOOL)isFirstResponderを使用します。

5
Johan Kool

Peter Steinbergerさんがプライベート通知UIWindowFirstResponderDidChangeNotificationについて つぶやく を報告しています。これはfirstResponderの変更を監視したい場合に確認できます。

4
Heath Borders

まさにそれがここにあるケースは素晴らしい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
    }

}
4

ユーザが背景領域をタップしたときにキーボードを消す必要があるだけの場合は、ジェスチャ認識機能を追加して[[self view] endEditing:YES]メッセージの送信に使用してはどうでしょうか。

タップジェスチャー認識機能をxibファイルまたはストーリーボードファイルに追加してアクションに関連付けることができます。

このようになり、その後終了しました

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

これは、ユーザーが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];
                }

            }
        }

    }

}
3
Romeo

これが私の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
Andrei Tchijov

UIResponderのカテゴリでは、UIApplicationオブジェクトに合法的に依頼して、最初のレスポンダが誰であるかを伝えることができます。

これを見なさい:

どのiOSの子にどのレスポンダステータスを持つのかをiOSビューに尋ねる方法はありますか?

2
VJK

あなたもこのように試すことができます:

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

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

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

試してみませんでしたが、良い解決策のようです

1
Frade

あなたはこのようにプライベートAPIを呼び出すことができます、Appleは無視します:

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

これは再帰の候補です。 UIViewにカテゴリを追加する必要はありません。

使用方法(View Controllerから)

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

Romeo https://stackoverflow.com/a/2799675/661022 による解決策はかっこいいですが、コードにもう1つループが必要であることに気付きました。私は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]);
                        }
                    }
                }
            }
        }
    }
}
1
pedrouan

私はこことはちょっと違うアプローチをしています。 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
MarqueIV

私はあなたと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;

}

1
Rubens Iotti

迅速なバージョンの @ 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
Kádi

あなたはそれを得るために次のUIView拡張子を選ぶことができます(Danielによるクレジット)

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

Swift 5では、UIResponderクラスに、現在アクティブなレスポンダを取得するために使用できる「現在の」オプション属性があります。こんな感じです:

UIResponder.current?.resignFirstResponder()

0
imattice

更新日:私は間違っていましたUIApplication.shared.sendAction(_:to:from:for:)を使って、このリンクに示されている最初のレスポンダを呼び出すことができます。 http://stackoverflow.com/a/14135456/74689


ここでの回答のほとんどは、ビュー階層にないと現在の最初のレスポンダを実際に見つけることができません。たとえば、AppDelegateまたはUIViewControllerサブクラスです。

最初のレスポンダオブジェクトがUIViewではない場合でも、確実に見つけることができる方法があります。

まず、nextUIResponderプロパティを使用して、その逆バージョンを実装します。

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

この計算されたプロパティで、たとえUIViewでなくても、現在の最初のレスポンダを下から上に見つけることができます。たとえば、View Controllerが最初のレスポンダの場合は、viewから管理しているUIViewControllerまでです。

ただし、現在のファーストレスポンダを取得するには、トップダウンの解決策、つまりvarが1つ必要です。

まずビュー階層について

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
yesleon

以下のコードは動作します。

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