在日常iOS開發中,系統提供的控制項常常無法滿足業務功能,這個時候需要我們實現一些自訂控制項。自訂控制項能讓我們完全控制視圖的展示內容以及互動操作。本篇將介紹一些自訂控制項的相關概念,探討自訂控制項開發的基本過程及技巧。
UIView
在開始之前我們先介紹一個類UIVew,它在iOS APP中佔有絕對重要的地位,因為幾乎所有的控制項都是繼承自UIView類。
UIView表示螢幕上的一個矩形地區,負責渲染地區內的內容,並且響應地區內發生的觸摸事件。
在UIView的內部有一個CALayer,提供內容的繪製和顯示,包括UIView的尺寸樣式。UIView的frame實際上返回的CALayer的frame。
UIView繼承自UIResponder類,它能接收並處理從系統傳來的事件,CALayer繼承自NSObject,它無法響應事件。所以UIView與CALayer的最大區別在於:UIView能響應事件,而CALayer不能。
更詳細的資料:https://developer.apple.com/reference/uikit/uiview
兩種實現方式
在建立自訂控制項時,主要有兩種實現方式,分別是純程式碼以及xib。接下來我們用這兩種方式分別示範一下建立自訂控制項的步驟。
我們實現一個簡單的demo ,效果如下,封裝一個圓形的imageView。
使用代碼建立自訂控制項
使用代碼建立自訂控制項,首先建立一個繼承自UIView的類
實現initWithFrame:方法。在該方法中,設定自訂控制項的屬性,並建立、添加子視圖:
-(instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; _imageView.contentMode = UIViewContentModeScaleAspectFill; _imageView.layer.masksToBounds = YES; _imageView.layer.cornerRadius = frame.size.width/2; [self addSubview:_imageView]; } return self;}
如果需要對子視圖重新布局,需要調用layoutSubViews方法:
-(void)layoutSubviews { [super layoutSubviews]; _imageView.frame = self.frame; _imageView.layer.cornerRadius = self.frame.size.width/2;}
layoutSubviews是調整子視圖布局的方法,官方文檔如下:
You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want.
意思是當你需要調整subview的大小的時候,重寫layoutSubviews方法。
layoutSubviews在以下情況下會被調用:
- init初始化不會觸發layoutSubviews
- addSubview會觸發layoutSubviews
- 設定view的Frame會觸發layoutSubviews,當然前提是frame的值設定前後發生了變化
- 滾動一個UIScrollView會觸發layoutSubviews
- 旋轉Screen會觸發父UIView上的layoutSubviews事件
- 改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件
這個自訂控制項提供對外介面方法,為自訂的控制項賦值
- (void)configeWithImage:(UIImage *)image { _imageView.image = image;}
最後,添加自訂控制項到頁面上
_circleImageView = [[CircleImageView alloc] initWithFrame:CGRectMake(0, 80, 150, 150)]; [_circleImageView configeWithImage:[UIImage imageNamed:@"tree"]]; [self.view addSubview:_circleImageView];
通過xib建立自訂控制項
首先建立一個自訂控制項XibCircleImageView,繼承自UIView
建立xib檔案,與XibCircleImageView類同名
配置xib中imageView的屬性,並將XibCircleImageView 類與對應的xib檔案進行綁定
代碼如下
- (void)awakeFromNib { [super awakeFromNib]; _imageView.layer.masksToBounds = YES; _imageView.layer.cornerRadius = self.frame.size.width/2; // [self addSubview:_imageView];}- (void)configeWithImage:(UIImage *)image { _imageView.image = image;}-(void)layoutSubviews { [super layoutSubviews]; _imageView.layer.cornerRadius = self.frame.size.width/2;}
在頁面中調用方式有點不同,通過loadNibNamed方法建立xib對象
//使用xib建立自訂控制項 _xibCircleImageView = [[[NSBundle mainBundle] loadNibNamed:@"XibCircleImageView" owner:nil options:nil] lastObject]; _xibCircleImageView.frame = CGRectMake(0, 500, 100, 100); [_xibCircleImageView configeWithImage:image]; [self.view addSubview:_xibCircleImageView];
當使用xib建立自訂控制項時,初始化不會調用initWithFrame:方法,只會調用initWithCoder:方法,初始化完畢後才調用awakeFromNib方法,注意要在awakeFromNib中初始化子控制項。因為initWithCoder:方法表示對象是從檔案解析來的,就會調用,而awakeFromNib方法是從xib或者storyboard載入完畢後才會調用。
小結
這兩種建立自訂控制項的方式各有優劣,純程式碼方式比較靈活,維護和擴充都比較方便,但寫起來比較麻煩。xib方式開發效率高,但不易擴充和維護,適合功能樣式比較穩定的自訂控制項。
事件傳遞機制
在自訂控制項中,可能需要動態響應事件,如按鈕太小,不易點擊,需要擴大按鈕的點擊範圍,接下來我們談談iOS的事件傳遞機制。
事件響應鏈
UIResponder類能夠響應觸摸、手勢以及遠端控制等事件。它是所有可響應事件的基類,其中包括很常見的UIView、UIViewController以及UIApplication。
UIResponder的屬性和方法如下圖,其中nextResponder表示指向一個UIResponder對象。
那麼事件響應鏈與UIResponder有什麼關係呢?應用內的視圖按一定的結構組織起來,即樹狀階層,一個視圖可以有多個子視圖,而子視圖只能有一個父視圖。當一個視圖被添加到父視圖上時。每一個視圖的nextResponder屬性就指向它的父視圖,這樣,整個應用就通過nextResponder串成了一條鏈,即響應鏈。響應鏈是一個虛擬鏈,並不是真實存在的,它藉助UIResponder的nextResponder串聯起來。如下圖
Hit-Test View
有了事件響應鏈,接下來就是尋找具體響應對象了,我們稱之為:Hit-Testing View,尋找這個View的過程稱為Hit-Test。
什麼是Hit-Test?我們可以把它理解為一個探測器,通過這個探測器,我們可以找到並判斷手指是否觸摸在某個視圖上。
Hit-Test是如何工作的?Hit-Test採用遞迴方式從視圖的根節點開始遍曆,直到找到某個點擊的視圖。
首先從UIWindow發送hitTest:withEvent:訊息開始,判斷該視圖是否能響應觸摸事件,如果不能響應返回nil,表示該視圖不能響應觸摸事件。然後再調用pointInside:withEvent:方法,該方法用於判斷觸摸事件點擊的位置是否處理該視圖範圍內,如果pointInside:withEvent:返回no,那麼hitTest:withEvent:也直接返回nil。
如果pointInside:withEvent: 方法返回yes,那麼該視圖向所有子視圖發送hitTest:withEvent:訊息,所有子視圖的調用順序是從最頂層視圖一直到最底層視圖,即從subViews的數組的末尾向前遍曆。直到有子視圖返回非Null 物件或全部遍曆完畢。若有子視圖返回非Null 物件,則hitTest:withEvent:方法返回該對象,處理結束;若所有子視圖都返回nil,則hitTest:withEvent:方法返回該視圖自身。
事件傳遞機制的應用
舉幾個例子,說明一下事件傳遞機制在自訂控制項中的應用。
一、擴大view的點擊地區。假設一個button的大小為20px 20px,太小難以點擊。我們通過重寫這個button子類的hitTest:withEvent:方法,判斷點擊處point是否在button周圍20px以內,如果是則返回自身,實現擴大點擊範圍的功能,代碼如下:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.hidden || self.alpha<=0.01) { return nil; } CGRect touchRect = CGRectInset(self.bounds, -20, -20); if (CGRectContainsPoint(touchRect, point)) { for (UIView *subView in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subView convertPoint:point toView:self]; UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil;}
二、穿透傳遞事件。
假設有兩個view,viewA和viewB,viewB完全覆蓋viewA,我們希望點擊viewB時能響應viewA的事件。我們重寫這個viewA的hitTest:withEvent:方法,不繼續遍曆它的子視圖,直接返回自身。代碼如下:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.hidden || self.alpha<=0.01) { return nil; } if ([self pointInside:point withEvent:event]) { NSLog(@"in view A"); return self; } return nil;}
回調機制
在自訂控制項開發中,需要向它的父類回傳傳回值。比如一個存放按鈕的自訂控制項,需要在上層接收按鈕點擊事件。我們可以使用多種方式回調訊息,比如target action模式、代理、block、通知等。
Target-Action
Target-Action是一種設計模式,當事件觸發時,它讓一個對象向另一個對象發送訊息。這個模式我們接觸的比較多,如為按鈕綁定點擊事件,為view添加手勢事件等。UIControl及其子類都支援這個機制。Target-Action 在訊息的寄件者和接收者之間建立了一個鬆散的關係。訊息的接收者不知道寄件者,甚至訊息的寄件者也不知道訊息的接收者會是什麼。
基於 target-action 傳遞機制的一個局限是,發送的訊息不能攜帶自訂的資訊。iOS 中,可以選擇性的把寄件者和觸發 action 的事件作為參數。除此之外就沒有別的控制 action 訊息內容的方法了。
舉個例子,我們使用Target-Action為控制項添加一個單擊手勢。
UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(refresh)]; [_imageView addGestureRecognizer:tapGR];- (void)refresh{ NSLog(@"Touch imageView");}
代理
代理是一種我們常用的回調方式,也是蘋果推薦的方式,在系統架構UIKit中大量使用,如UITableView、UITextField。
優點:1,代理文法清晰,可讀性高,易於維護 ;2,它減少了代碼耦合性,使事件監聽與事件處理分離;3,一個控制器可以實現多個代理,滿足自訂開發需求,靈活性較高;
缺點:1,實現代理的過程較繁瑣;2,跨層傳值時加大代碼的耦合性,並且程式的階層也變得混亂;3,當多個對象同時傳值時不易區分,導致代理易用性大大降低;
Block
Block封裝一段代碼,併當做變數進行傳遞,它十分方便地將不同地方的程式碼群組織在一起,可讀性很高。
優點:1,文法簡潔,代碼可讀性和可維護性較高。2,配合GCD優秀的解決多線程問題。
缺點:1,Block中得代碼將自動進行一次retain操作,容易造成記憶體泄露。 2.Block內預設引用為強引用,容易造成循環參考。
通知
代理是一對一的關係,通知是一對多的關係,通知相比代理可以實現更大跨度的通訊機制。但接收對象多了,就難以控制,有時不希望的對象也接收處理了訊息。
優點:1,使用簡單,代碼精簡。2,支援一對多,解決了同時向多個對象監聽的問題。3,傳值方便快捷,Context自身攜帶相應的內容。
缺點:1,通知使用完畢後需要登出,否則會造成意外崩潰。2,key不夠安全,編譯器不會檢測到是否被通知中樞正確處理。3,調試時難以跟蹤。 4,當使用者向通知中樞發送通知的時候,並不能獲得任何反饋資訊。 5.需要一個第三方的對象來做監聽者與被監聽者的中介。
總結
至此,開發自訂控制項的相關知識梳理了一遍,希望能協助大家更好地理解自訂控制項開發。