Получите текущий ответчик без использования частного API
-
22-07-2019 - |
Вопрос
Я отправил заявку чуть больше недели назад и сегодня получил письмо с ужасным отказом.Он сообщает мне, что мое приложение не может быть принято, поскольку я использую закрытый API;в частности, там сказано,
Частный API, включенный в ваше приложение, — firstResponder.
Теперь вызывающий нарушение вызов API на самом деле является решением, которое я нашел здесь, на SO:
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *firstResponder = [keyWindow performSelector:@selector(firstResponder)];
Как вывести на экран текущего сотрудника службы экстренного реагирования?Я ищу способ, благодаря которому мое приложение не будет отклонено.
Решение
В одном из моих приложений я часто хочу, чтобы первый ответчик ушел в отставку, если пользователь нажмет на фон.Для этой цели я написал категорию в 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`
}
Другие советы
Если вашей конечной целью является просто отставка первого респондента, это должно сработать: [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
Хитрость здесь в том, что отправка действия в ноль отправляет его первому респонденту.
(Первоначально я опубликовал этот ответ здесь: https://stackoverflow.com/a/14135456/322427)
Вот расширение, реализованное в 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, когда я не знаю, что это за ответчик:
Создайте UITextField, либо в IB, либо программно. Сделать это скрытым Свяжите его с вашим кодом, если вы сделали это в IB.
Затем, когда вы хотите закрыть клавиатуру, вы переключаете респондент в невидимое текстовое поле и сразу же отмените его:
[self.invisibleField becomeFirstResponder];
[self.invisibleField resignFirstResponder];
Для Swift 3 & amp; 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
Первым респондентом может быть любой экземпляр класса 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 не был найден в указанных подпредставлениях просмотров.
Выполните итерацию по представлениям, которые могут быть первым респондентом, и используйте - (BOOL) isFirstResponder
, чтобы определить, являются ли они в данный момент.
Питер Стейнбергер просто написал в Твиттере о частном уведомлении UIWindowFirstResponderDidChangeNotification
, Вы можете наблюдать, хотите ли вы увидеть изменение firstResponder.
Если вам просто нужно убить клавиатуру, когда пользователь нажимает на фоновую область, почему бы не добавить распознаватель жестов и использовать его для отправки сообщения [[self view] endEditing: YES]
? р>
вы можете добавить распознаватель жестов касания в файл xib или раскадровки и подключить его к действию,
выглядит примерно так, потом закончил
- (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer{
[[self view] endEditing:YES];
}
В данном случае 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
}
}
Это то, что я сделал, чтобы выяснить, какой 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];
}
}
}
}
}
Это то, что есть в моей категории 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
, чтобы получить его (кредит от Daniel) :
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 классное, но я заметил что коду нужен еще один цикл. Я работал с 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.
Использование (из вашего контроллера представления):
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;
}
Вы можете вызвать конфиденциальный 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;
}
Swift версия @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
}
}
У меня немного другой подход, чем у большинства здесь. Вместо того, чтобы перебирать коллекцию представлений в поисках того, для которого установлено 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
.
Сначала давайте реализуем его обратную версию, используя свойство 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 { extension UIResponder {
static var first: UIResponder? {
return UIApplication.shared.windows.compactMap({ let firstResponder = UIResponder.first
.previousFirstResponder }).first
}
}
.previousFirstResponder }.first
}
}
Это будет искать первого респондента в обратном направлении, и если он не сможет его найти, он скажет своим подпредставлениям сделать то же самое (потому что next
его подпредставления не обязательно само по себе). При этом мы можем найти его из любого представления, включая UIWindow
.
И, наконец, мы можем построить это:
<*>Поэтому, когда вы хотите получить первого респондента, вы можете позвонить:
<*>