UIKit: UIControl

我們在開發應用的時候,經常會用到各種各樣的控件,諸如按鈕(UIButton)、滑塊(UISlider)、分頁控件(UIPageControl)等。這些控件用來與用戶進行交互,響應用戶的操作。我們查看這些類的繼承體系,可以看到它們都是繼承于UIControl類。UIControl是控件類的基類,它是一個抽象基類,我們不能直接使用UIControl類來實例化控件,它只是為控件子類定義一些通用的接口,并提供一些基礎實現,以在事件發生時,預處理這些消息并將它們發送到指定目標對象上。

本文將通過一個自定義的UIControl子類來看看UIControl的基本使用方法。不過在開始之前,讓我們先來了解一下Target-Action機制。

Target-Action機制

Target-action是一種設計模式,直譯過來就是”目標-行為”。當我們通過代碼為一個按鈕添加一個點擊事件時,通常是如下處理:

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


即當事件發生時,事件會被發送到控件對象中,然后再由這個控件對象去觸發target對象上的action行為,來最終處理事件。因此,Target-Action機制由兩部分組成:即目標對象和行為Selector。目標對象指定最終處理事件的對象,而行為Selector則是處理事件的方法。

有關Target-Action機制的具體描述,大家可以參考Cocoa Application Competencies for iOS – Target Action。我們將會在下面討論一些Target-action更深入的東西。

實例:一個帶Label的圖片控件

回到我們的正題來,我們將實現一個帶Label的圖片控件。通常情況下,我們會基于以下兩個原因來實現一個自定義的控件:

對于特定的事件,我們需要觀察或修改分發到target對象的行為消息。

提供自定義的跟蹤行為。

本例將會簡單地結合這兩者。先來看看效果:


這個控件很簡單,以圖片為背景,然后在下方顯示一個Label。

先創建UIControl的一個子類,我們需要傳入一個字符串和一個UIImage對象:

@interfaceImageControl:UIControl

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

@end

基礎的布局我們在此不討論。我們先來看看UIControl為我們提供了哪些自定義跟蹤行為的方法。

跟蹤觸摸事件

如果是想提供自定義的跟蹤行為,則可以重寫以下幾個方法:

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

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

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

-(void)cancelTrackingWithEvent:(UIEvent*)event

這四個方法分別對應的時跟蹤開始、移動、結束、取消四種狀態。看起來是不是很熟悉?這跟UIResponse提供的四個事件跟蹤方法是不是挺像的?我們來看看UIResponse的四個方法:

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

我們可以看到,上面兩組方法的參數基本相同,只不過UIControl的是針對單點觸摸,而UIResponse可能是多點觸摸。另外,返回值也是大同小異。由于UIControl本身是視圖,所以它實際上也繼承了UIResponse的這四個方法。如果測試一下,我們會發現在針對控件的觸摸事件發生時,這兩組方法都會被調用,而且互不干涉。

為了判斷當前對象是否正在追蹤觸摸操作,UIControl定義了一個tracking屬性。該值如果為YES,則表明正在追蹤。這對于我們是更加方便了,不需要自己再去額外定義一個變量來做處理。

在測試中,我們可以發現當我們的觸摸點沿著屏幕移出控件區域名,還是會繼續追蹤觸摸操作,cancelTrackingWithEvent:消息并未被發送。為了判斷當前觸摸點是否在控件區域類,可以使用touchInside屬性,這是個只讀屬性。不過實測的結果是,在控件區域周邊一定范圍內,該值還是會被標記為YES,即用于判定touchInside為YES的區域會比控件區域要大。

觀察或修改分發到target對象的行為消息

對于一個給定的事件,UIControl會調用sendAction:to:forEvent:來將行為消息轉發到UIApplication對象,再由UIApplication對象調用其sendAction:to:fromSender:forEvent:方法來將消息分發到指定的target上,而如果我們沒有指定target,則會將事件分發到響應鏈上第一個想處理消息的對象上。而如果子類想監控或修改這種行為的話,則可以重寫這個方法。

在我們的實例中,做了個小小的處理,將外部添加的Target-Action放在控件內部來處理事件,因此,我們的代碼實現如下:

// 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:實際上也被UIControl的另一個方法所調用,即sendActionsForControlEvents:。這個方法的作用是發送與指定類型相關的所有行為消息。我們可以在任意位置(包括控件內部和外部)調用控件的這個方法來發送參數controlEvents指定的消息。在我們的示例中,在ViewController.m中作了如下測試:

-(void)viewDidLoad{

// ...

[controladdTarget:selfaction:@selector(tapImageControl:)forControlEvents:UIControlEventTouchUpInside];

[controlsendActionsForControlEvents:UIControlEventTouchUpInside];

}

可以看到在未點擊控件的情況下,觸發了UIControlEventTouchUpInside事件,并打印了handle Action日志。

Target-Action的管理

// 添加

-(void)addTarget:(id)targetaction:(SEL)actionforControlEvents:(UIControlEvents)controlEvents

-(void)removeTarget:(id)targetaction:(SEL)actionforControlEvents:(UIControlEvents)controlEvents

如果想獲取控件對象所有相關的target對象,則可以調用allTargets方法,該方法返回一個集合。集合中可能包含NSNull對象,表示至少有一個nil目標對象。

而如果想獲取某個target對象及事件相關的所有action,則可以調用actionsForTarget:forControlEvent:方法。

不過,這些都是UIControl開放出來的接口。我們還是想要探究一下,UIControl是如何去管理Target-Action的呢?

實際上,我們在程序某個合適的位置打個斷點來觀察UIControl的內部結構,可以看到這樣的結果:

因此,UIControl內部實際上是有一個可變數組(_targetActions)來保存Target-Action,數組中的每個元素是一個UIControlTargetAction對象。UIControlTargetAction類是一個私有類,我們可以在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對象維護了一個Target-Action所必須的三要素,即target,action及對應的事件eventMask。

如果仔細想想,會發現一個有意思的問題。我們來看看實例中ViewController(target)與ImageControl實例(control)的引用關系,如下圖所示:


嗯,循環引用。

既然這樣,就必須想辦法打破這種循環引用。那么在這5個環節中,哪個地方最適合做這件事呢?仔細思考一樣,1、2、4肯定是不行的,3也不太合適,那就只有5了。在上面的UIControlTargetAction頭文件中,并沒有辦法看出_target是以weak方式聲明的,那有證據么?

我們在工程中打個Symbolic斷點,如下所示:


運行程序,程序會進入[UIControl addTarget:action:forControlEvents:]方法的匯編代碼頁,在這里,我們可以找到一些蛛絲馬跡。如下圖所示:


可以看到,對于_target成員變量,在UIControlTargetAction的初始化方法中調用了objc_storeWeak,即這個成員變量對外部傳進來的target對象是以weak的方式引用的。

其實在UIControl的文檔中,addTarget:action:forControlEvents:方法的說明還有這么一句:

When you call this method, target is not retained.

另外,如果我們以同一組target-action和event多次調用addTarget:action:forControlEvents:方法,在_targetActions中并不會重復添加UIControlTargetAction對象。

小結

控件是我們在開發中常用的視圖工具,能很好的表達用戶的意圖。我們可以使用UIKit提供的控件,也可以自定義控件。當然,UIControl除了上述的一些方法,還有一些屬性和方法,以及一些常量,大家可以參考文檔。

示例工程的代碼已上傳到github,可以在這里下載。另外,推薦一下SVSegmentedControl這個控件,大家可以研究下它的實現。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容