プライベートAPIを使用せずに現在のファーストレスポンダーを取得する
-
22-07-2019 - |
質問
1週間ほど前にアプリを提出しましたが、今日、恐ろしい拒否メールが届きました。非公開のAPIを使用しているため、アプリが受け入れられないことがわかります。具体的には、
アプリケーションに含まれている非パブリックAPIはfirstResponderです。
今、問題のAPI呼び出しは、実際に私がSOで見つけた解決策です:
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *firstResponder = [keyWindow performSelector:@selector(firstResponder)];
現在のファーストレスポンダーを画面に表示するにはどうすればよいですか?アプリが拒否されない方法を探しています。
解決
私のアプリケーションの1つでは、ユーザーがバックグラウンドをタップした場合、最初のレスポンダーが辞任することがよくあります。このために、UIViewで呼び出すUIViewにカテゴリを作成しました。
以下はそれに基づいており、最初のレスポンダーを返す必要があります。
@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`
}
他のヒント
最終的な目的が最初のレスポンダーを辞任することだけであれば、これは機能するはずです: [self.view endEditing:YES]
最初のレスポンダーを操作する一般的な方法は、nilターゲットアクションを使用することです。これは、任意のメッセージをレスポンダーチェーンに送信し(最初のレスポンダーから開始)、誰かがメッセージに応答するまでチェーンを続けていく方法です(セレクターに一致するメソッドが実装されています)。
キーボードを閉じる場合、これはどのウィンドウまたはビューが最初のレスポンダーであるかに関係なく動作する最も効果的な方法です:
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
これは、 [self.view.window endEditing:YES]
よりも効果的です。
( BigZaphod にこのコンセプトを思い出させてくれてありがとう)
[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 )
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
}
}
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
}
}
見栄えはよくありませんが、レスポンダーが何であるかわからない場合のfirstResponderの辞任方法:
IBまたはプログラムでUITextFieldを作成します。非表示にします。 IBで作成した場合は、コードにリンクします。
次に、キーボードを閉じるには、レスポンダーを非表示のテキストフィールドに切り替え、すぐに辞職します:
[self.invisibleField becomeFirstResponder];
[self.invisibleField resignFirstResponder];
Swift 3&amp;の場合 nevyn's の4つの回答:
UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
正しいファーストレスポンダーを報告するソリューションがあります(たとえば、他の多くのソリューションはファーストレスポンダーとして 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
ファーストレスポンダーは、UIResponderクラスの任意のインスタンスにすることができるため、UIViewsにもかかわらずファーストレスポンダーになる可能性のある他のクラスがあります。たとえば、 UIViewController
も最初のレスポンダーかもしれません。
この要点では、最初のレスポンダーをアプリケーションのウィンドウのrootViewControllerから始まるコントローラーの階層。
次の操作により、最初のレスポンダーを取得できます
- (void)foo
{
// Get the first responder
id firstResponder = [UIResponder firstResponder];
// Do whatever you want
[firstResponder resignFirstResponder];
}
ただし、最初のレスポンダーがUIViewまたはUIViewControllerのサブクラスでない場合、このアプローチは失敗します。
この問題を修正するには、 UIResponder
にカテゴリを作成して別のアプローチを実行し、このクラスのすべての生きているインスタンスの配列を構築できるようにいくつかの魔法のスウィズリングを実行します。次に、最初のレスポンダーを取得するために、単純に反復し、各オブジェクトに -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)
結果はオプション値であるため、指定されたビューのサブビューでfirstResponserが見つからなかった場合はnilになることに注意してください。
最初のレスポンダーになる可能性のあるビューを反復処理し、-(BOOL)isFirstResponder
を使用して、現在のビューかどうかを判断します。
Peter Steinbergerは、プライベート通知 UIWindowFirstResponderDidChangeNotification
についてツイートしました。 firstResponderの変更を確認するかどうかを確認できます。
ユーザーが背景領域をタップしたときにキーボードを強制終了する必要がある場合は、ジェスチャー認識機能を追加し、それを使用して [[self view] endEditing:YES]
メッセージを送信してください。
xibまたはストーリーボードファイルにタップジェスチャ認識機能を追加して、アクションに接続できます
このように見えてから終了
- (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];
}
}
}
}
}
これは、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
オブジェクトに最初のレスポンダーが誰であるかを合法的に尋ねることができます。
こちらをご覧ください:
次の UIView
拡張機能を選択して、(ダニエルによるクレジット)を取得できます。
extension UIView {
var firstResponder: UIView? {
guard !isFirstResponder else { return self }
return subviews.first(where: {<*>.firstResponder != 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 からのソリューションはクールですが、私は気づきましたコードにはもう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]);
}
}
}
}
}
}
}
これは再帰の良い候補です! 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;
}
このようにprivate apiを呼び出すことができます、apple ignore:
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&#252; 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
}
}
ここでのアプローチとは少し異なるアプローチをしています。 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
}
以下のコードは機能します。
- (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 。
ここでの回答のほとんどは、現在のファーストレスポンダーがビュー階層にない場合、実際には見つかりません。たとえば、 AppDelegate
または UIViewController
サブクラス。
最初のレスポンダーオブジェクトが UIView
でない場合でも、確実に検索できる方法があります。
まず、 UIResponder
の next
プロパティを使用して、その逆バージョンを実装します。
extension UIResponder {
var nextFirstResponder: UIResponder? {
return isFirstResponder ? self : next?.nextFirstResponder
}
}
この計算されたプロパティを使用すると、 UIView
でなくても、現在の最初のレスポンダーを下から上に見つけることができます。たとえば、 view
から、それを管理している UIViewController
へ(View Controllerが最初のレスポンダーである場合)。
ただし、現在のファーストレスポンダーを取得するには、トップダウン解像度、単一の var
が必要です。
最初にビュー階層を使用:
extension UIView {
var previousFirstResponder: UIResponder? {
return nextFirstResponder ?? subviews.compactMap { extension UIResponder {
static var first: UIResponder? {
return UIApplication.shared.windows.compactMap({ let firstResponder = UIResponder.first
.previousFirstResponder }).first
}
}
.previousFirstResponder }.first
}
}
これは最初のレスポンダーを後方に検索し、見つからなかった場合、サブビューに同じことを行うように指示します(サブビューの next
は必ずしもそれ自体ではないため)。これにより、 UIWindow
を含む任意のビューから見つけることができます。
そして最後に、これをビルドできます:
<*>したがって、最初のレスポンダーを取得する場合は、次のように呼び出すことができます。
<*>