iOS自定義控件開發(fā)梳理總結
在日常iOS開發(fā)中,系統(tǒng)提供的控件常常無法滿足業(yè)務功能,這個時候需要我們實現(xiàn)一些自定義控件。自定義控件能讓我們完全控制視圖的展示內容以及交互操作。本篇將介紹一些自定義控件的相關概念,探討自定義控件開發(fā)的基本過程及技巧。
UIView
在開始之前我們先介紹一個類UIVew,它在iOS APP中占有絕對重要的地位,因為幾乎所有的控件都是繼承自UIView類。
UIView表示屏幕上的一個矩形區(qū)域,負責渲染區(qū)域內的內容,并且響應區(qū)域內發(fā)生的觸摸事件。
在UIView的內部有一個CALayer,提供內容的繪制和顯示,包括UIView的尺寸樣式。UIView的frame實際上返回的CALayer的frame。
UIView繼承自UIResponder類,它能接收并處理從系統(tǒng)傳來的事件,CALayer繼承自NSObject,它無法響應事件。所以UIView與CALayer的最大區(qū)別在于:UIView能響應事件,而CALayer不能。
更詳細的資料:https://developer.apple.com/reference/uikit/uiview
兩種實現(xiàn)方式
在創(chuàng)建自定義控件時,主要有兩種實現(xiàn)方式,分別是純代碼以及xib。接下來我們用這兩種方式分別演示一下創(chuàng)建自定義控件的步驟。
我們實現(xiàn)一個簡單的demo ,效果如下,封裝一個圓形的imageView。
使用代碼創(chuàng)建自定義控件
使用代碼創(chuàng)建自定義控件,首先創(chuàng)建一個繼承自UIView的類
實現(xiàn)initWithFrame:方法。在該方法中,設置自定義控件的屬性,并創(chuàng)建、添加子視圖:
-(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初始化不會觸發(fā)layoutSubviews
- addSubview會觸發(fā)layoutSubviews
- 設置view的Frame會觸發(fā)layoutSubviews,當然前提是frame的值設置前后發(fā)生了變化
- 滾動一個UIScrollView會觸發(fā)layoutSubviews
- 旋轉Screen會觸發(fā)父UIView上的layoutSubviews事件
- 改變一個UIView大小的時候也會觸發(fā)父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創(chuàng)建自定義控件
首先創(chuàng)建一個自定義控件XibCircleImageView,繼承自UIView
創(chuàng)建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方法創(chuàng)建xib對象
//使用xib創(chuàng)建自定義控件 _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創(chuàng)建自定義控件時,初始化不會調用initWithFrame:方法,只會調用initWithCoder:方法,初始化完畢后才調用awakeFromNib方法,注意要在awakeFromNib中初始化子控件。因為initWithCoder:方法表示對象是從文件解析來的,就會調用,而awakeFromNib方法是從xib或者storyboard加載完畢后才會調用。
小結
這兩種創(chuàng)建自定義控件的方式各有優(yōu)劣,純代碼方式比較靈活,維護和擴展都比較方便,但寫起來比較麻煩。xib方式開發(fā)效率高,但不易擴展和維護,適合功能樣式比較穩(wěn)定的自定義控件。
事件傳遞機制
在自定義控件中,可能需要動態(tài)響應事件,如按鈕太小,不易點擊,需要擴大按鈕的點擊范圍,接下來我們談談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采用遞歸方式從視圖的根節(jié)點開始遍歷,直到找到某個點擊的視圖。
首先從UIWindow發(fā)送hitTest:withEvent:消息開始,判斷該視圖是否能響應觸摸事件,如果不能響應返回nil,表示該視圖不能響應觸摸事件。然后再調用pointInside:withEvent:方法,該方法用于判斷觸摸事件點擊的位置是否處理該視圖范圍內,如果pointInside:withEvent:返回no,那么hitTest:withEvent:也直接返回nil。
如果pointInside:withEvent: 方法返回yes,那么該視圖向所有子視圖發(fā)送hitTest:withEvent:消息,所有子視圖的調用順序是從最頂層視圖一直到最底層視圖,即從subViews的數(shù)組的末尾向前遍歷。直到有子視圖返回非空對象或全部遍歷完畢。若有子視圖返回非空對象,則hitTest:withEvent:方法返回該對象,處理結束;若所有子視圖都返回nil,則hitTest:withEvent:方法返回該視圖自身。
事件傳遞機制的應用
舉幾個例子,說明一下事件傳遞機制在自定義控件中的應用。
一、擴大view的點擊區(qū)域。假設一個button的大小為20px 20px,太小難以點擊。我們通過重寫這個button子類的hitTest:withEvent:方法,判斷點擊處point是否在button周圍20px以內,如果是則返回自身,實現(xiàn)擴大點擊范圍的功能,代碼如下:
-(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:方法,不繼續(xù)遍歷它的子視圖,直接返回自身。代碼如下:
-(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; }
回調機制
在自定義控件開發(fā)中,需要向它的父類回傳返回值。比如一個存放按鈕的自定義控件,需要在上層接收按鈕點擊事件。我們可以使用多種方式回調消息,比如target action模式、代理、block、通知等。
Target-Action
Target-Action是一種設計模式,當事件觸發(fā)時,它讓一個對象向另一個對象發(fā)送消息。這個模式我們接觸的比較多,如為按鈕綁定點擊事件,為view添加手勢事件等。UIControl及其子類都支持這個機制。Target-Action 在消息的發(fā)送者和接收者之間建立了一個松散的關系。消息的接收者不知道發(fā)送者,甚至消息的發(fā)送者也不知道消息的接收者會是什么。
基于 target-action 傳遞機制的一個局限是,發(fā)送的消息不能攜帶自定義的信息。iOS 中,可以選擇性的把發(fā)送者和觸發(fā) action 的事件作為參數(shù)。除此之外就沒有別的控制 action 消息內容的方法了。
舉個例子,我們使用Target-Action為控件添加一個單擊手勢。
UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(refresh)]; [_imageView addGestureRecognizer:tapGR]; - (void)refresh{ NSLog(@"Touch imageView"); }
代理
代理是一種我們常用的回調方式,也是蘋果推薦的方式,在系統(tǒng)框架UIKit中大量使用,如UITableView、UITextField。
優(yōu)點:1,代理語法清晰,可讀性高,易于維護 ;2,它減少了代碼耦合性,使事件監(jiān)聽與事件處理分離;3,一個控制器可以實現(xiàn)多個代理,滿足自定義開發(fā)需求,靈活性較高;
缺點:1,實現(xiàn)代理的過程較繁瑣;2,跨層傳值時加大代碼的耦合性,并且程序的層次結構也變得混亂;3,當多個對象同時傳值時不易區(qū)分,導致代理易用性大大降低;
Block
Block封裝一段代碼,并當做變量進行傳遞,它十分方便地將不同地方的代碼組織在一起,可讀性很高。
優(yōu)點:1,語法簡潔,代碼可讀性和可維護性較高。2,配合GCD優(yōu)秀的解決多線程問題。
缺點:1,Block中得代碼將自動進行一次retain操作,容易造成內存泄露。 2.Block內默認引用為強引用,容易造成循環(huán)引用。
通知
代理是一對一的關系,通知是一對多的關系,通知相比代理可以實現(xiàn)更大跨度的通信機制。但接收對象多了,就難以控制,有時不希望的對象也接收處理了消息。
優(yōu)點:1,使用簡單,代碼精簡。2,支持一對多,解決了同時向多個對象監(jiān)聽的問題。3,傳值方便快捷,Context自身攜帶相應的內容。
缺點:1,通知使用完畢后需要注銷,否則會造成意外崩潰。2,key不夠安全,編譯器不會檢測到是否被通知中心正確處理。3,調試時難以跟蹤。 4,當使用者向通知中心發(fā)送通知的時候,并不能獲得任何反饋信息。 5.需要一個第三方的對象來做監(jiān)聽者與被監(jiān)聽者的中介。
總結
至此,開發(fā)自定義控件的相關知識梳理了一遍,希望能幫助大家更好地理解自定義控件開發(fā)。
相關文章
IOS 下獲取 rootviewcontroller 的版本不同的問題解決辦法
這篇文章主要介紹了IOS 下獲取 rootviewcontroller 的版本不同的問題解決辦法的相關資料,希望通過本文能幫助到大家,讓大家遇到這種問題可以解決,需要的朋友可以參考下2017-10-10IOS開發(fā)UIPasteboard類的粘貼板全面詳解
這篇文章主要為大家介紹了IOS開發(fā)UIPasteboard類的粘貼板全面詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06IOS中Weex 加載 .xcassets 中的圖片資源的實例詳解
這篇文章主要介紹了IOS中Weex 加載 .xcassets 中的圖片資源的實例詳解的相關資料,希望通過本文介紹能幫助到大家,實現(xiàn)這樣的功能,需要的朋友可以參考下2017-08-08