Получить текущий первый респондент без использования частного API
Я отправил свое приложение чуть более недели назад и получил страшное письмо с отказом сегодня. Это говорит мне, что мое приложение не может быть принято, потому что я использую непубличный API; в частности, это говорит,
Непубличный API, включенный в ваше приложение, называется firstResponder.
Теперь вызов API-интерфейса на самом деле является решением, которое я нашел здесь на SO:
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *firstResponder = [keyWindow performSelector:@selector(firstResponder)];
Как вывести текущего первого респондента на экран? Я ищу способ, которым мое приложение не будет отклонено.
26 ответов
В одном из моих приложений я часто хочу, чтобы первый респондент подал в отставку, если пользователь нажимает на фон. Для этого я написал категорию на 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`
}
Если вашей конечной целью является просто подать в отставку первого респондента, это должно работать: [self.view endEditing:YES]
Распространенным способом манипулирования первым респондентом является использование нулевых целевых действий. Это способ отправки произвольного сообщения в цепочку респондента (начиная с первого респондента) и продолжения вниз по цепочке, пока кто-то не ответит на сообщение (реализовал метод, соответствующий селектору).
В случае отклонения клавиатуры это наиболее эффективный способ, который будет работать независимо от того, какое окно или представление является первым респондентом:
[[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 отправляет его первому респонденту.
(Я первоначально опубликовал этот ответ здесь: /questions/19221993/est-li-sposob-sprosit-u-ios-kto-iz-ee-detej-imeet-status-pervogo-respondenta/19222002#19222002)
Вот расширение, реализованное в Swift на основе самого превосходного ответа Якоба Эггера:
import UIKit
extension UIResponder {
// Swift 1.2 finally supports static vars!. If you use 1.1 see:
// http://stackru.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, когда я не знаю, что это за ответчик:
Создайте UITextField, либо в IB, либо программно. Сделать это скрытым Свяжите это с вашим кодом, если вы сделали это в IB.
Затем, когда вы хотите убрать клавиатуру, вы переключаете респондент в невидимое текстовое поле и немедленно отмените его:
[self.invisibleField becomeFirstResponder];
[self.invisibleField resignFirstResponder];
Для Swift 3 и 4 версии ответа Невина:
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
Используя 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 не был найден в данных подпредставлениях представлений.
Первым респондентом может быть любой экземпляр класса UIResponder, поэтому существуют другие классы, которые могут быть первым респондентом, несмотря на UIViews. Например UIViewController
также может быть первым ответчиком.
В этом разделе вы найдете рекурсивный способ получения первого респондента, просматривая иерархию контроллеров, начиная с rootViewController окон приложения.
Вы можете получить первого респондента, выполнив
- (void)foo
{
// Get the first responder
id firstResponder = [UIResponder firstResponder];
// Do whatever you want
[firstResponder resignFirstResponder];
}
Однако, если первый респондент не является подклассом UIView или UIViewController, этот подход потерпит неудачу.
Чтобы решить эту проблему, мы можем сделать другой подход, создав категорию на UIResponder
и выполнить магическое изобилие, чтобы иметь возможность собрать множество всех живых экземпляров этого класса. Затем, чтобы получить первого респондента, мы можем просто повторить и спросить каждый объект, если -isFirstResponder
,
Этот подход может быть найден реализованным в этой другой сути.
Надеюсь, поможет.
У меня немного другой подход, чем у большинства здесь. Вместо того, чтобы перебирать коллекцию представлений, ища ту, которая имеет 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
}
Переберите взгляды, которые могут быть первым респондентом и используйте - (BOOL)isFirstResponder
чтобы определить, если они в настоящее время.
Питер Штайнбергер только что написал в Твиттере о личном уведомлении UIWindowFirstResponderDidChangeNotification
, который вы можете наблюдать, если вы хотите наблюдать за первым изменением ответа.
В данном случае 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
}
}
Если вам просто нужно убить клавиатуру, когда пользователь нажимает на фоновую область, почему бы не добавить распознаватель жестов и использовать его для отправки [[self view] endEditing:YES]
сообщение?
вы можете добавить распознаватель жестов касания в файл xib или раскадровки и подключить его к действию,
выглядит примерно так потом закончил
- (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer{
[[self view] endEditing:YES];
}
Это то, что я сделал, чтобы узнать, что UITextField является firstResponder, когда пользователь нажимает Сохранить / Отмена в ModalViewController:
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];
}
}
}
}
}
Это хороший кандидат на рекурсию! Нет необходимости добавлять категорию в 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;
}
Это то, что у меня есть в моей категории 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, кто из ее детей имеет статус первого респондента?
Вы можете выбрать следующие UIView
расширение, чтобы получить его (предоставлено Даниэлем):
extension UIView {
var firstResponder: UIView? {
guard !isFirstResponder else { return self }
return subviews.first(where: {$0.firstResponder != nil })
}
}
Обновление: я был неправ. Вы действительно можете использовать UIApplication.shared.sendAction(_:to:from:for:)
позвонить первому респонденту, указанному по этой ссылке: /questions/19221993/est-li-sposob-sprosit-u-ios-kto-iz-ee-detej-imeet-status-pervogo-respondenta/19222002#19222002.
Большинство ответов здесь не могут найти текущего первого респондента, если он не находится в иерархии представлений. Например, AppDelegate
или же UIViewController
подклассы.
Существует способ гарантировать его нахождение, даже если первый объект респондента не является UIView
,
Сначала давайте реализуем его обратную версию, используя next
собственностью UIResponder
:
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
Вы также можете попробовать вот так:
- (void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event {
for (id textField in self.view.subviews) {
if ([textField isKindOfClass:[UITextField class]] && [textField isFirstResponder]) {
[textField resignFirstResponder];
}
}
}
Я не пробовал, но это кажется хорошим решением
Быстрая версия ответа @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
}
}
Вы можете назвать 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;
}
Решение от romeo /questions/17715620/poluchit-tekuschij-pervyij-respondent-bez-ispolzovaniya-chastnogo-api/17715641#17715641 - это круто, но я заметил, что коду нужен еще один цикл. Я работал с 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;
}
Самый простой способ найти первого респондента:
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
для ваших собственных целей.