UIKit: UIControl

我們?cè)陂_(kāi)發(fā)應(yīng)用的時(shí)候像鸡,經(jīng)常會(huì)用到各種各樣的控件,諸如按鈕(UIButton)漏策、滑塊(UISlider)派哲、分頁(yè)控件(UIPageControl)等。這些控件用來(lái)與用戶進(jìn)行交互掺喻,響應(yīng)用戶的操作芭届。我們查看這些類的繼承體系,可以看到它們都是繼承于UIControl類巢寡。UIControl是控件類的基類喉脖,它是一個(gè)抽象基類,我們不能直接使用UIControl類來(lái)實(shí)例化控件抑月,它只是為控件子類定義一些通用的接口树叽,并提供一些基礎(chǔ)實(shí)現(xiàn),以在事件發(fā)生時(shí)谦絮,預(yù)處理這些消息并將它們發(fā)送到指定目標(biāo)對(duì)象上题诵。

本文將通過(guò)一個(gè)自定義的UIControl子類來(lái)看看UIControl的基本使用方法。不過(guò)在開(kāi)始之前层皱,讓我們先來(lái)了解一下Target-Action機(jī)制性锭。

Target-Action機(jī)制

Target-action是一種設(shè)計(jì)模式,直譯過(guò)來(lái)就是”目標(biāo)-行為”叫胖。當(dāng)我們通過(guò)代碼為一個(gè)按鈕添加一個(gè)點(diǎn)擊事件時(shí)草冈,通常是如下處理:

[buttonaddTarget:selfaction:@selector(tapButton:)forControlEvents:UIControlEventTouchUpInside];


即當(dāng)事件發(fā)生時(shí),事件會(huì)被發(fā)送到控件對(duì)象中,然后再由這個(gè)控件對(duì)象去觸發(fā)target對(duì)象上的action行為怎棱,來(lái)最終處理事件哩俭。因此,Target-Action機(jī)制由兩部分組成:即目標(biāo)對(duì)象和行為Selector拳恋。目標(biāo)對(duì)象指定最終處理事件的對(duì)象凡资,而行為Selector則是處理事件的方法。

有關(guān)Target-Action機(jī)制的具體描述谬运,大家可以參考Cocoa Application Competencies for iOS – Target Action隙赁。我們將會(huì)在下面討論一些Target-action更深入的東西。

實(shí)例:一個(gè)帶Label的圖片控件

回到我們的正題來(lái)梆暖,我們將實(shí)現(xiàn)一個(gè)帶Label的圖片控件伞访。通常情況下,我們會(huì)基于以下兩個(gè)原因來(lái)實(shí)現(xiàn)一個(gè)自定義的控件:

對(duì)于特定的事件式廷,我們需要觀察或修改分發(fā)到target對(duì)象的行為消息咐扭。

提供自定義的跟蹤行為。

本例將會(huì)簡(jiǎn)單地結(jié)合這兩者滑废。先來(lái)看看效果:


這個(gè)控件很簡(jiǎn)單蝗肪,以圖片為背景,然后在下方顯示一個(gè)Label蠕趁。

先創(chuàng)建UIControl的一個(gè)子類薛闪,我們需要傳入一個(gè)字符串和一個(gè)UIImage對(duì)象:

@interfaceImageControl:UIControl

-(instancetype)initWithFrame:(CGRect)frametitle:(NSString*)titleimage:(UIImage*)image;

@end

基礎(chǔ)的布局我們?cè)诖瞬挥懻摗N覀兿葋?lái)看看UIControl為我們提供了哪些自定義跟蹤行為的方法俺陋。

跟蹤觸摸事件

如果是想提供自定義的跟蹤行為豁延,則可以重寫(xiě)以下幾個(gè)方法:

-(BOOL)beginTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event

-(BOOL)continueTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event

-(void)endTrackingWithTouch:(UITouch*)touchwithEvent:(UIEvent*)event

-(void)cancelTrackingWithEvent:(UIEvent*)event

這四個(gè)方法分別對(duì)應(yīng)的時(shí)跟蹤開(kāi)始、移動(dòng)腊状、結(jié)束诱咏、取消四種狀態(tài)〗赏冢看起來(lái)是不是很熟悉袋狞?這跟UIResponse提供的四個(gè)事件跟蹤方法是不是挺像的?我們來(lái)看看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ù)基本相同苟鸯,只不過(guò)UIControl的是針對(duì)單點(diǎn)觸摸,而UIResponse可能是多點(diǎn)觸摸棚点。另外早处,返回值也是大同小異。由于UIControl本身是視圖瘫析,所以它實(shí)際上也繼承了UIResponse的這四個(gè)方法砌梆。如果測(cè)試一下默责,我們會(huì)發(fā)現(xiàn)在針對(duì)控件的觸摸事件發(fā)生時(shí),這兩組方法都會(huì)被調(diào)用么库,而且互不干涉傻丝。

為了判斷當(dāng)前對(duì)象是否正在追蹤觸摸操作甘有,UIControl定義了一個(gè)tracking屬性诉儒。該值如果為YES,則表明正在追蹤亏掀。這對(duì)于我們是更加方便了忱反,不需要自己再去額外定義一個(gè)變量來(lái)做處理。

在測(cè)試中滤愕,我們可以發(fā)現(xiàn)當(dāng)我們的觸摸點(diǎn)沿著屏幕移出控件區(qū)域名温算,還是會(huì)繼續(xù)追蹤觸摸操作,cancelTrackingWithEvent:消息并未被發(fā)送间影。為了判斷當(dāng)前觸摸點(diǎn)是否在控件區(qū)域類注竿,可以使用touchInside屬性,這是個(gè)只讀屬性魂贬。不過(guò)實(shí)測(cè)的結(jié)果是巩割,在控件區(qū)域周邊一定范圍內(nèi),該值還是會(huì)被標(biāo)記為YES付燥,即用于判定touchInside為YES的區(qū)域會(huì)比控件區(qū)域要大宣谈。

觀察或修改分發(fā)到target對(duì)象的行為消息

對(duì)于一個(gè)給定的事件,UIControl會(huì)調(diào)用sendAction:to:forEvent:來(lái)將行為消息轉(zhuǎn)發(fā)到UIApplication對(duì)象键科,再由UIApplication對(duì)象調(diào)用其sendAction:to:fromSender:forEvent:方法來(lái)將消息分發(fā)到指定的target上闻丑,而如果我們沒(méi)有指定target扩劝,則會(huì)將事件分發(fā)到響應(yīng)鏈上第一個(gè)想處理消息的對(duì)象上片橡。而如果子類想監(jiān)控或修改這種行為的話棋返,則可以重寫(xiě)這個(gè)方法编饺。

在我們的實(shí)例中扛禽,做了個(gè)小小的處理捉片,將外部添加的Target-Action放在控件內(nèi)部來(lái)處理事件赞赖,因此甜害,我們的代碼實(shí)現(xiàn)如下:

// ImageControl.m

-(void)sendAction:(SEL)actionto:(id)targetforEvent:(UIEvent*)event{

// 將事件傳遞到對(duì)象本身來(lái)處理

[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);

}

由于我們重寫(xiě)了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è)方法來(lái)發(fā)送參數(shù)controlEvents指定的消息硝拧。在我們的示例中径筏,在ViewController.m中作了如下測(cè)試:

-(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

如果想獲取控件對(duì)象所有相關(guān)的target對(duì)象,則可以調(diào)用allTargets方法抱究,該方法返回一個(gè)集合恢氯。集合中可能包含NSNull對(duì)象,表示至少有一個(gè)nil目標(biāo)對(duì)象鼓寺。

而如果想獲取某個(gè)target對(duì)象及事件相關(guān)的所有action勋拟,則可以調(diào)用actionsForTarget:forControlEvent:方法。

不過(guò)妈候,這些都是UIControl開(kāi)放出來(lái)的接口敢靡。我們還是想要探究一下,UIControl是如何去管理Target-Action的呢苦银?

實(shí)際上啸胧,我們?cè)诔绦蚰硞€(gè)合適的位置打個(gè)斷點(diǎn)來(lái)觀察UIControl的內(nèi)部結(jié)構(gòu),可以看到這樣的結(jié)果:

因此幔虏,UIControl內(nèi)部實(shí)際上是有一個(gè)可變數(shù)組(_targetActions)來(lái)保存Target-Action纺念,數(shù)組中的每個(gè)元素是一個(gè)UIControlTargetAction對(duì)象。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對(duì)象維護(hù)了一個(gè)Target-Action所必須的三要素柠辞,即target,action及對(duì)應(yīng)的事件eventMask主胧。

如果仔細(xì)想想叭首,會(huì)發(fā)現(xiàn)一個(gè)有意思的問(wèn)題。我們來(lái)看看實(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頭文件中氯窍,并沒(méi)有辦法看出_target是以weak方式聲明的饲常,那有證據(jù)么?

我們?cè)诠こ讨写騻€(gè)Symbolic斷點(diǎn)狼讨,如下所示:


運(yùn)行程序贝淤,程序會(huì)進(jìn)入[UIControl addTarget:action:forControlEvents:]方法的匯編代碼頁(yè),在這里政供,我們可以找到一些蛛絲馬跡播聪。如下圖所示:


可以看到,對(duì)于_target成員變量鲫骗,在UIControlTargetAction的初始化方法中調(diào)用了objc_storeWeak犬耻,即這個(gè)成員變量對(duì)外部傳進(jìn)來(lái)的target對(duì)象是以weak的方式引用的。

其實(shí)在UIControl的文檔中执泰,addTarget:action:forControlEvents:方法的說(shuō)明還有這么一句:

When you call this method, target is not retained.

另外,如果我們以同一組target-action和event多次調(diào)用addTarget:action:forControlEvents:方法渡蜻,在_targetActions中并不會(huì)重復(fù)添加UIControlTargetAction對(duì)象术吝。

小結(jié)

控件是我們?cè)陂_(kāi)發(fā)中常用的視圖工具,能很好的表達(dá)用戶的意圖茸苇。我們可以使用UIKit提供的控件排苍,也可以自定義控件。當(dāng)然学密,UIControl除了上述的一些方法淘衙,還有一些屬性和方法,以及一些常量腻暮,大家可以參考文檔彤守。

示例工程的代碼已上傳到github,可以在這里下載哭靖。另外具垫,推薦一下SVSegmentedControl這個(gè)控件,大家可以研究下它的實(shí)現(xiàn)试幽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筝蚕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子铺坞,更是在濱河造成了極大的恐慌起宽,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件济榨,死亡現(xiàn)場(chǎng)離奇詭異坯沪,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)腿短,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)屏箍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)绘梦,“玉大人,你說(shuō)我怎么就攤上這事赴魁⌒斗睿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵颖御,是天一觀的道長(zhǎng)榄棵。 經(jīng)常有香客問(wèn)我,道長(zhǎng)潘拱,這世上最難降的妖魔是什么疹鳄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮芦岂,結(jié)果婚禮上瘪弓,老公的妹妹穿的比我還像新娘。我一直安慰自己禽最,他們只是感情好腺怯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著川无,像睡著了一般呛占。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上懦趋,一...
    開(kāi)封第一講書(shū)人閱讀 51,258評(píng)論 1 300
  • 那天晾虑,我揣著相機(jī)與錄音,去河邊找鬼仅叫。 笑死帜篇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惑芭。 我是一名探鬼主播坠狡,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼遂跟!你這毒婦竟也來(lái)了逃沿?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤幻锁,失蹤者是張志新(化名)和其女友劉穎凯亮,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體哄尔,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡假消,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岭接。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片富拗。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡臼予,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出啃沪,到底是詐尸還是另有隱情粘拾,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布创千,位于F島的核電站缰雇,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏追驴。R本人自食惡果不足惜械哟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望殿雪。 院中可真熱鬧暇咆,春花似錦、人聲如沸冠摄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)河泳。三九已至,卻和暖如春年栓,著一層夾襖步出監(jiān)牢的瞬間拆挥,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工某抓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纸兔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓否副,卻偏偏與公主長(zhǎng)得像汉矿,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子备禀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容