前言
在iOS中,當(dāng)發(fā)生事件響應(yīng)時抱环,必須知道由誰來響應(yīng)事件壳快。而UIResponder類就是專門用來響應(yīng)用戶的操作,處理各種事件的镇草,包括觸摸事件(Touch Events)眶痰、運動事件(Motion Events)和遠程控制事件(Remote Control Events)。iOS處理事件的流程將遵循一個不同對象組成的層次結(jié)構(gòu)梯啤,也就是響應(yīng)者鏈(Responder Chain)竖伯,網(wǎng)上目前有很多關(guān)于響應(yīng)者鏈的介紹,這里就不再細講因宇。在響應(yīng)者鏈中非常重要的一個概念就是第一響應(yīng)者(First Responder)七婴,當(dāng)前第一響應(yīng)者負責(zé)響應(yīng)事件,或?qū)⑹录鬟f給下一響應(yīng)者察滑。
在編寫iOS程序時打厘,我們經(jīng)常會遇到需要獲取當(dāng)前的第一響應(yīng)者,例如系統(tǒng)彈出鍵盤時贺辰,我們希望得到當(dāng)前輸入框(也就是第一響應(yīng)者)的Frame户盯,從而調(diào)整視圖避免鍵盤遮擋輸入框。然而UIKit并沒有提供官方的API專門用于該用途饲化。本文將介紹一種非常簡單的且未用到私有API的方法來獲取當(dāng)前第一響應(yīng)者莽鸭。
實現(xiàn)思路
常規(guī)思路
通過遍歷當(dāng)前UIWindow的所有子視圖,從而找到當(dāng)前的第一響應(yīng)者吃靠。這種方法首先需要做非常多的遞歸調(diào)用硫眨,從而判斷所有子視圖,同時當(dāng)前響應(yīng)鏈上的第一響應(yīng)者還有可能是子視圖的ViewController巢块,這種方法也會漏掉礁阁。
使用私有API的思路
使用蘋果的私有API可以很容易地解決這個問題巧号,然而蘋果不會允許使用私有API的App上架App Store,而且私有API很有可能隨時變化氮兵,所以這種方式也很不完美裂逐。
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *firstResponder = [keyWindow performSelector:@selector(firstResponder)];
本文的思路
本文的思路用到的核心Public API就是
- (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event
蘋果文檔對該API的target參數(shù)的描述如下:
The object to receive the action message. If target is nil, the app sends the message to the first responder, from whence it progresses up the responder chain until it is handled.
從而可知,利用該API泣栈,只要將傳入的target設(shè)置為nil卜高,則系統(tǒng)會自動順著響應(yīng)鏈查找能夠響應(yīng)action的響應(yīng)者。我們只需讓所有UIResponder的子類都響應(yīng)我們自定義的action南片,即可知道當(dāng)前第一響應(yīng)者是哪個對象掺涛。
實現(xiàn)方法
為實現(xiàn)本文的思路,我們需要為UIResponder提供一個Category(objc)或者Extension(swift)疼进。
Objective-C
// UIResponder+WTYFirstResponder.h
#import <UIKit/UIKit.h>
@interface UIResponder (WTYFirstResponder)
//使用時只需要對UIResponder類調(diào)用該類方法即可獲得當(dāng)前第一響應(yīng)者
+ (id)wty_currentFirstResponder;
@end
// UIResponder+WTYFirstResponder.m
#import "UIResponder+WTYFirstResponder.h"
static __weak id wty_currentFirstResponder;
@implementation UIResponder (WTYFirstResponder)
+ (id)wty_currentFirstResponder {
wty_currentFirstResponder = nil;
// 通過將target設(shè)置為nil薪缆,讓系統(tǒng)自動遍歷響應(yīng)鏈
// 從而響應(yīng)鏈當(dāng)前第一響應(yīng)者響應(yīng)我們自定義的方法
[[UIApplication sharedApplication] sendAction:@selector(wty_findFirstResponder:)
to:nil
from:nil
forEvent:nil];
return wty_currentFirstResponder;
}
- (void)wty_findFirstResponder:(id)sender {
// 第一響應(yīng)者會響應(yīng)這個方法,并且將靜態(tài)變量wty_currentFirstResponder設(shè)置為自己
wty_currentFirstResponder = self;
}
@end
使用方法
#import "UIResponder+WTYFirstResponder.h"
id firstResponder = [UIResponder wty_firstResponder];
Swift
// UIResponder+WTYFirstResponder.swift
import UIKit
private weak var wty_currentFirstResponder: AnyObject?
extension UIResponder {
static func wty_firstResponder() -> AnyObject? {
wty_currentFirstResponder = nil
// 通過將target設(shè)置為nil伞广,讓系統(tǒng)自動遍歷響應(yīng)鏈
// 從而響應(yīng)鏈當(dāng)前第一響應(yīng)者響應(yīng)我們自定義的方法
UIApplication.shared.sendAction(#selector(wty_findFirstResponder(_:)), to: nil, from: nil, for: nil)
return wty_currentFirstResponder
}
func wty_findFirstResponder(_ sender: AnyObject) {
// 第一響應(yīng)者會響應(yīng)這個方法拣帽,并且將靜態(tài)變量wty_currentFirstResponder設(shè)置為自己
wty_currentFirstResponder = self
}
}
使用方法
firstResponder = UIResponder.wty_firstResponder()
思路衍生
如果只希望讓第一響應(yīng)者取消其第一響應(yīng)者的狀態(tài),則可以做如下操作:
Objective-C
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
Swift
UIApplication.shared.sendAction(#selector(resignFirstResponder), to: nil, from: nil, for: nil)
Github Repo
本文所用的代碼可以在我的Github上找到嚼锄,如果覺得好用的話請并忙點個星星减拭。
本文個人博客地址: http://wty.im/2016/09/22/get-the-first-responder/
Github: https://github.com/wty21cn/