UIControl 的基本使用方法和 Target-Action 機制

jopen 8年前發布 | 12K 次閱讀 UIControl iOS開發 移動開發

作者: 南峰子

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

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

Target-Action機制

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

[button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside];

也就是說,當按鈕的點擊事件發生時,會將消息發送到 target (此處即為self對象),并由 target 對象的 tapButton: 方法來處理相應的事件。其基本過程可以用下圖來描述:

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

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

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

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

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

  • 提供自定義的跟蹤行為。

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

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

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

@interface ImageControl : UIControl

- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title image:(UIImage *)image;

@end

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

跟蹤觸摸事件

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

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
- (void)cancelTrackingWithEvent:(UIEvent *)event

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

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesCancelled:(NSSet *)touches withEvent:(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)action to:(id)target forEvent:(UIEvent *)event {
  // 將事件傳遞到對象本身來處理
    [super sendAction:@selector(handleAction:) to:self forEvent:event];
}

- (void)handleAction:(id)sender {

    NSLog(@"handle Action");
}

// ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];

    ImageControl *control = [[ImageControl alloc] initWithFrame:(CGRect){50.0f, 100.0f, 200.0f, 300.0f} title:@"This is a demo" image:[UIImage imageNamed:@"demo"]];
    // ...

    [control addTarget:self action:@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 {
    // ...
    [control addTarget:self action:@selector(tapImageControl:) forControlEvents:UIControlEventTouchUpInside];

    [control sendActionsForControlEvents:UIControlEventTouchUpInside];
}

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

Target-Action的管理

為一個控件對象添加、刪除 Target-Action 的操作我們都已經很熟悉了,主要使用的是以下兩個方法:

// 添加
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents

- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents

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

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

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

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

因此, UIControl 內部實際上是有一個可變數組( _targetActions )來保存 Target-Action ,數組中的每個元素是一個 UIControlTargetAction 對象。 UIControlTargetAction 類是一個私有類,我們可以在 iOS-Runtime-Header 中找到它的頭文件:

@interface UIControlTargetAction : NSObject {
    SEL _action;
    BOOL _cancelled;
    unsigned int _eventMask;
    id _target;
}

@property (nonatomic) BOOL cancelled;

- (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 這個控件,大家可以研究下它的實現。

參考

  1. UIControl Class Reference

  2. UIKit User Interface Catalog – About Controls

  3. Cocoa Application Competencies for iOS – Target Action

  4. iOS-Runtime-Header: UIControlTargetAction

  5. SVSegmentedControl

來自: http://www.cocoachina.com/ios/20160111/14932.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!