我們在開發(fā)應(yīng)用的時(shí)候,經(jīng)常會(huì)用到各種各樣的控件,諸如按鈕(UIButton)、滑塊(UISlider)、分頁控件(UIPageControl)等。這些控件用來與用戶進(jìn)行交互,響應(yīng)用戶的操作。我們查看這些類的繼承體系,可以看到它們都是繼承于UIControl類。UIControl是控件類的基類,它是一個(gè)抽象基類,我們不能直接使用UIControl類來實(shí)例化控件,它只是為控件子類定義一些通用的接口,并提供一些基礎(chǔ)實(shí)現(xiàn),以在事件發(fā)生時(shí),預(yù)處理這些消息并將它們發(fā)送到指定目標(biāo)對象上。
本文將通過一個(gè)自定義的UIControl子類來看看UIControl的基本使用方法。不過在開始之前,讓我們先來了解一下Target-Action機(jī)制。
Target-Action機(jī)制
Target-action是一種設(shè)計(jì)模式,直譯過來就是”目標(biāo)-行為”。當(dāng)我們通過代碼為一個(gè)按鈕添加一個(gè)點(diǎn)擊事件時(shí),通常是如下處理:
[buttonaddTarget:selfaction:@selector(tapButton:)forControlEvents:UIControlEventTouchUpInside];
即當(dāng)事件發(fā)生時(shí),事件會(huì)被發(fā)送到控件對象中,然后再由這個(gè)控件對象去觸發(fā)target對象上的action行為,來最終處理事件。因此,Target-Action機(jī)制由兩部分組成:即目標(biāo)對象和行為Selector。目標(biāo)對象指定最終處理事件的對象,而行為Selector則是處理事件的方法。
有關(guān)Target-Action機(jī)制的具體描述,大家可以參考Cocoa Application Competencies for iOS – Target Action。我們將會(huì)在下面討論一些Target-action更深入的東西。
實(shí)例:一個(gè)帶Label的圖片控件
回到我們的正題來,我們將實(shí)現(xiàn)一個(gè)帶Label的圖片控件。通常情況下,我們會(huì)基于以下兩個(gè)原因來實(shí)現(xiàn)一個(gè)自定義的控件:
對于特定的事件,我們需要觀察或修改分發(fā)到target對象的行為消息。
提供自定義的跟蹤行為。
本例將會(huì)簡單地結(jié)合這兩者。先來看看效果:
這個(gè)控件很簡單,以圖片為背景,然后在下方顯示一個(gè)Label。
先創(chuàng)建UIControl的一個(gè)子類,我們需要傳入一個(gè)字符串和一個(gè)UIImage對象:
@interfaceImageControl:UIControl
-(instancetype)initWithFrame:(CGRect)frametitle:(NSString*)titleimage:(UIImage*)image;
@end
基礎(chǔ)的布局我們在此不討論。我們先來看看UIControl為我們提供了哪些自定義跟蹤行為的方法。
跟蹤觸摸事件
如果是想提供自定義的跟蹤行為,則可以重寫以下幾個(gè)方法:
-(BOOL)beginTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event
-(BOOL)continueTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event
-(void)endTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event
-(void)cancelTrackingWithEvent:(UIEvent*)event
這四個(gè)方法分別對應(yīng)的時(shí)跟蹤開始、移動(dòng)、結(jié)束、取消四種狀態(tài)。看起來是不是很熟悉?這跟UIResponse提供的四個(gè)事件跟蹤方法是不是挺像的?我們來看看UIResponse的四個(gè)方法:
-(void)touchesBegan:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event
-(void)touchesMoved:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event
-(void)touchesEnded:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event
-(void)touchesCancelled:(NSSet<UITouch*>*)toucheswithEvent:(UIEvent*)event
我們可以看到,上面兩組方法的參數(shù)基本相同,只不過UIControl的是針對單點(diǎn)觸摸,而UIResponse可能是多點(diǎn)觸摸。另外,返回值也是大同小異。由于UIControl本身是視圖,所以它實(shí)際上也繼承了UIResponse的這四個(gè)方法。如果測試一下,我們會(huì)發(fā)現(xiàn)在針對控件的觸摸事件發(fā)生時(shí),這兩組方法都會(huì)被調(diào)用,而且互不干涉。
為了判斷當(dāng)前對象是否正在追蹤觸摸操作,UIControl定義了一個(gè)tracking屬性。該值如果為YES,則表明正在追蹤。這對于我們是更加方便了,不需要自己再去額外定義一個(gè)變量來做處理。
在測試中,我們可以發(fā)現(xiàn)當(dāng)我們的觸摸點(diǎn)沿著屏幕移出控件區(qū)域名,還是會(huì)繼續(xù)追蹤觸摸操作,cancelTrackingWithEvent:消息并未被發(fā)送。為了判斷當(dāng)前觸摸點(diǎn)是否在控件區(qū)域類,可以使用touchInside屬性,這是個(gè)只讀屬性。不過實(shí)測的結(jié)果是,在控件區(qū)域周邊一定范圍內(nèi),該值還是會(huì)被標(biāo)記為YES,即用于判定touchInside為YES的區(qū)域會(huì)比控件區(qū)域要大。
觀察或修改分發(fā)到target對象的行為消息
對于一個(gè)給定的事件,UIControl會(huì)調(diào)用sendAction:to:forEvent:來將行為消息轉(zhuǎn)發(fā)到UIApplication對象,再由UIApplication對象調(diào)用其sendAction:to:fromSender:forEvent:方法來將消息分發(fā)到指定的target上,而如果我們沒有指定target,則會(huì)將事件分發(fā)到響應(yīng)鏈上第一個(gè)想處理消息的對象上。而如果子類想監(jiān)控或修改這種行為的話,則可以重寫這個(gè)方法。
在我們的實(shí)例中,做了個(gè)小小的處理,將外部添加的Target-Action放在控件內(nèi)部來處理事件,因此,我們的代碼實(shí)現(xiàn)如下:
// ImageControl.m
-(void)sendAction:(SEL)actionto:(id)targetforEvent:(UIEvent*)event{
// 將事件傳遞到對象本身來處理
[supersendAction:@selector(handleAction:)to:selfforEvent:event];
}
-(void)handleAction:(id)sender{
NSLog(@"handle Action");
}
// ViewController.m
-(void)viewDidLoad{
[superviewDidLoad];
self.view.backgroundColor=[UIColorwhiteColor];
ImageControl*control=[[ImageControlalloc]initWithFrame:(CGRect){50.0f,100.0f,200.0f,300.0f}title:@"This is a demo"image:[UIImageimageNamed:@"demo"]];
// ...
[controladdTarget:selfaction:@selector(tapImageControl:)forControlEvents:UIControlEventTouchUpInside];
}
-(void)tapImageControl:(id)sender{
NSLog(@"sender = %@",sender);
}
由于我們重寫了sendAction:to:forEvent:方法,所以最后處理事件的Selector是ImageControl的handleAction:方法,而不是ViewController的tapImageControl:方法。
另外,sendAction:to:forEvent:實(shí)際上也被UIControl的另一個(gè)方法所調(diào)用,即sendActionsForControlEvents:。這個(gè)方法的作用是發(fā)送與指定類型相關(guān)的所有行為消息。我們可以在任意位置(包括控件內(nèi)部和外部)調(diào)用控件的這個(gè)方法來發(fā)送參數(shù)controlEvents指定的消息。在我們的示例中,在ViewController.m中作了如下測試:
-(void)viewDidLoad{
// ...
[controladdTarget:selfaction:@selector(tapImageControl:)forControlEvents:UIControlEventTouchUpInside];
[controlsendActionsForControlEvents:UIControlEventTouchUpInside];
}
可以看到在未點(diǎn)擊控件的情況下,觸發(fā)了UIControlEventTouchUpInside事件,并打印了handle Action日志。
Target-Action的管理
// 添加
-(void)addTarget:(id)targetaction:(SEL)actionforControlEvents:(UIControlEvents)controlEvents
-(void)removeTarget:(id)targetaction:(SEL)actionforControlEvents:(UIControlEvents)controlEvents
如果想獲取控件對象所有相關(guān)的target對象,則可以調(diào)用allTargets方法,該方法返回一個(gè)集合。集合中可能包含NSNull對象,表示至少有一個(gè)nil目標(biāo)對象。
而如果想獲取某個(gè)target對象及事件相關(guān)的所有action,則可以調(diào)用actionsForTarget:forControlEvent:方法。
不過,這些都是UIControl開放出來的接口。我們還是想要探究一下,UIControl是如何去管理Target-Action的呢?
實(shí)際上,我們在程序某個(gè)合適的位置打個(gè)斷點(diǎn)來觀察UIControl的內(nèi)部結(jié)構(gòu),可以看到這樣的結(jié)果:
因此,UIControl內(nèi)部實(shí)際上是有一個(gè)可變數(shù)組(_targetActions)來保存Target-Action,數(shù)組中的每個(gè)元素是一個(gè)UIControlTargetAction對象。UIControlTargetAction類是一個(gè)私有類,我們可以在iOS-Runtime-Header中找到它的頭文件:
@interfaceUIControlTargetAction:NSObject{
SEL_action;
BOOL_cancelled;
unsignedint_eventMask;
id_target;
}
@property(nonatomic)BOOLcancelled;
-(void).cxx_destruct;
-(BOOL)cancelled;
-(void)setCancelled:(BOOL)arg1;
@end
可以看到UIControlTargetAction對象維護(hù)了一個(gè)Target-Action所必須的三要素,即target,action及對應(yīng)的事件eventMask。
如果仔細(xì)想想,會(huì)發(fā)現(xiàn)一個(gè)有意思的問題。我們來看看實(shí)例中ViewController(target)與ImageControl實(shí)例(control)的引用關(guān)系,如下圖所示:
嗯,循環(huán)引用。
既然這樣,就必須想辦法打破這種循環(huán)引用。那么在這5個(gè)環(huán)節(jié)中,哪個(gè)地方最適合做這件事呢?仔細(xì)思考一樣,1、2、4肯定是不行的,3也不太合適,那就只有5了。在上面的UIControlTargetAction頭文件中,并沒有辦法看出_target是以weak方式聲明的,那有證據(jù)么?
我們在工程中打個(gè)Symbolic斷點(diǎn),如下所示:
運(yùn)行程序,程序會(huì)進(jìn)入[UIControl addTarget:action:forControlEvents:]方法的匯編代碼頁,在這里,我們可以找到一些蛛絲馬跡。如下圖所示:
可以看到,對于_target成員變量,在UIControlTargetAction的初始化方法中調(diào)用了objc_storeWeak,即這個(gè)成員變量對外部傳進(jìn)來的target對象是以weak的方式引用的。
其實(shí)在UIControl的文檔中,addTarget:action:forControlEvents:方法的說明還有這么一句:
When you call this method, target is not retained.
另外,如果我們以同一組target-action和event多次調(diào)用addTarget:action:forControlEvents:方法,在_targetActions中并不會(huì)重復(fù)添加UIControlTargetAction對象。
小結(jié)
控件是我們在開發(fā)中常用的視圖工具,能很好的表達(dá)用戶的意圖。我們可以使用UIKit提供的控件,也可以自定義控件。當(dāng)然,UIControl除了上述的一些方法,還有一些屬性和方法,以及一些常量,大家可以參考文檔。
示例工程的代碼已上傳到github,可以在這里下載。另外,推薦一下SVSegmentedControl這個(gè)控件,大家可以研究下它的實(shí)現(xiàn)。