在日常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
http://www.cocoachina.com/ios/20150828/13244.html
兩種實現方式
在創建自定義控件時,主要有兩種實現方式,分別是純代碼以及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在以下情況下會被調用:
1、init初始化不會觸發layoutSubviews
2、addSubview會觸發layoutSubviews
3、設置view的Frame會觸發layoutSubviews,當然前提是frame的值設置前後發生了變化
4、滾動一個UIScrollView會觸發layoutSubviews
5、旋轉Screen會觸發父UIView上的layoutSubviews事件
6、改變一個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加載完畢後才會調用。
本文demo地址:https://github.com/superzcj/ZCJCustomViewDemo
小結
這兩種創建自定義控件的方式各有優劣,純代碼方式比較靈活,維護和擴展都比較方便,但寫起來比較麻煩。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的數組的末尾向前遍歷。直到有子視圖返回非空對象或全部遍歷完畢。若有子視圖返回非空對象,則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; }
更詳細的資料:
http://zhoon.github.io/ios/2015/04/12/ios-event.html
http://www.jianshu.com/p/2f664e71c527
回調機制
在自定義控件開發中,需要向它的父類回傳返回值。比如一個存放按鈕的自定義控件,需要在上層接收按鈕點擊事件。我們可以使用多種方式回調消息,比如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。
優點:
代理語法清晰,可讀性高,易於維護 ;
它減少了代碼耦合性,使事件監聽與事件處理分離;
一個控制器可以實現多個代理,滿足自定義開發需求,靈活性較高;
缺點:
實現代理的過程較繁瑣;
跨層傳值時加大代碼的耦合性,並且程序的層次結構也變得混亂;
當多個對象同時傳值時不易區分,導致代理易用性大大降低;
Block
Block封裝一段代碼,並當做變量進行傳遞,它十分方便地將不同地方的代碼組織在一起,可讀性很高。
優點:1,語法簡潔,代碼可讀性和可維護性較高。2,配合GCD優秀的解決多純程問題。
缺點:1,Block中得代碼將自動進行一次retain操作,容易造成內存洩露。 2.Block內默認引用為強引用,容易造成循環引用。
通知
代理是一對一的關系,通知是一對多的關系,通知相比代理可以實現更大跨度的通信機制。但接收對象多了,就難以控制,有時不希望的對象也接收處理了消息。
優點:
使用簡單,代碼精簡。
支持一對多,解決了同時向多個對象監聽的問題。
傳值方便快捷,Context自身攜帶相應的內容。
缺點:
通知使用完畢後需要注銷,否則會造成意外崩潰。
key不夠安全,編譯器不會檢測到是否被通知中心正確處理。
調試時難以跟蹤。
當使用者向通知中心發送通知的時候,並不能獲得任何反饋信息。
需要一個第三方的對象來做監聽者與被監聽者的中介。
更詳細的資料:
https://objccn.io/issue-3-4/
http://maru-zhang.tk/2015/06/08/iOS-Development-Delegate,Notification,Block/
總結
至此,開發自定義控件的相關知識梳理了一遍,希望能幫助大家更好地理解自定義控件開發。