1. 程式人生 > >如何設計一個 iOS 控制元件? iOS 控制元件完全解析

如何設計一個 iOS 控制元件? iOS 控制元件完全解析

程式碼的等級:可編譯、可執行、可測試、可讀、可維護、可複用

前言

一個控制元件從外在特徵來說,主要是封裝這幾點:

  • 互動方式
  • 顯示樣式
  • 資料使用

對外在特徵的封裝,能讓我們在多種環境下達到 PM 對產品的要求,並且提到程式碼複用率,使維護工作保持在一個相對較小的範圍內;而一個好的控制元件除了有對外一致的體驗之外,還有其內在特徵:

  • 靈活性
  • 低耦合
  • 易拓展
  • 易維護

通常特徵之間需要做一些取捨,比如靈活性與耦合度,有時候介面越多越能適應各種環境,但是介面越少對外產生的依賴就越少,維護起來也更容易。通常一些前期看起來還不錯的程式碼,往往也會隨著時間加深慢慢“成長”,功能的增加也會帶來新的介面,很不自覺地就加深了耦合度,在開發中時不時地進行一些重構工作很有必要。總之,儘量減少介面的數量,但有足夠的定製空間,可以在一開始把介面全部隱藏起來,再根據實際需要慢慢放開。

自定義控制元件在 iOS 專案裡很常見,通常頁面之間入口很多,而且使用場景極有可能大不相同,比如一個 UIView 既可以以程式碼初始化,也可以以 xib 的形式初始化,而我們是需要保證這兩種操作都能產生同樣的行為。本文將會討論到以下幾點:

  • 選擇正確的初始化方式
  • 調整佈局的時機
  • 正確的處理 touches 方法
  • drawRectCALayer 與動畫
  • UIControl 與 UIButton
  • 更友好的支援 xib
  • 不規則圖形和事件觸發範圍(事件鏈的簡單介紹以及處理)
  • 合理使用 KVO

如果這些問題你一看就懂的話就不用繼續往下看了。

設計方針

選擇正確的初始化方式

UIView 的首要問題就是既能從程式碼中初始化,也能從 xib

中初始化,兩者有何不同? UIView 是支援 NSCoding 協議的,當在 xib 或 storyboard 裡存在一個 UIView 的時候,其實是將 UIView 序列化到檔案裡(xib 和 storyboard 都是以 XML 格式來儲存的),載入的時候反序列化出來,所以:

  • 當從程式碼例項化 UIView 的時候,initWithFrame 會執行;
  • 當從檔案載入 UIView 的時候,initWithCoder 會執行。

從程式碼中載入

雖然 initWithFrame 是 UIView 的Designated Initializer,理論上來講你繼承自 UIView 的任何子類,該方法最終都會被呼叫,但是有一些類在初始化的時候沒有遵守這個約定,如 UIImageView

initWithImageUITableViewCellinitWithStyle:reuseIdentifier: 的構造器等,所以我們在寫自定義控制元件的時候,最好只假設父檢視的 Designated Initializer 被呼叫。

如果控制元件在初始化或者在使用之前必須有一些引數要設定,那我們可以寫自己的 Designated Initializer 構造器,如:

Objective-C
1 -(instancetype)initWithName:(NSString*)name;

在實現中一定要呼叫父類的 Designated Initializer,而且如果你有多個自定義的 Designated Initializer,最終都應該指向一個全能的初始化構造器:

Objective-C
123456789101112 -(instancetype)initWithName:(NSString*)name{self=[self initWithName:name frame:CGRectZero];returnself;}-(instancetype)initWithName:(NSString*)name frame:(CGRect)frame{self=[super initWithFrame:frame];if(self){self.name=name;}returnself;}

並且你要考慮到,因為你的控制元件是繼承自 UIView 或 UIControl 的,那麼使用者完全可以不使用你提供的構造器,而直接呼叫基類的構造器,所以最好重寫父類的 Designated Initializer,使它呼叫你提供的 Designated Initializer ,比如父類是個 UIView:

Objective-C
1234 -(instancetype)initWithFrame:(CGRect)frame{self=[self initWithName:nil frame:frame];returnself;}

這樣當用戶從程式碼裡初始化你的控制元件的時候,就總是逃脫不了你需要執行的初始化程式碼了,哪怕使用者直接呼叫 init 方法,最終還是會回到父類的 Designated Initializer 上。

從 xib 或 storyboard 中載入

當控制元件從 xib 或 storyboard 中載入的時候,情況就變得複雜了,首先我們知道有 initWithCoder 方法,該方法會在物件被反序列化的時候呼叫,比如從檔案載入一個 UIView 的時候:

Objective-C
123456789 UIView*view=[[UIViewalloc] init];NSData*data=[NSKeyedArchiver archivedDataWithRootObject:view];[[NSUserDefaultsstandardUserDefaults] setObject:data forKey:@"KeyView"];[[NSUserDefaultsstandardUserDefaults] synchronize];data=[[NSUserDefaultsstandardUserDefaults] objectForKey:@"KeyView"];view=[NSKeyedUnarchiver unarchiveObjectWithData:data];NSLog(@"%@",view);

執行 unarchiveObjectWithData 的時候, initWithCoder 會被呼叫,那麼你有可能會在這個方法裡做一些初始化工作,比如恢復到儲存之前的狀態,當然前提是需要在 encodeWithCoder 中預先儲存下來。

不過我們很少會自己直接把一個 View 儲存到檔案中,一般是在 xib 或 storyboard 中寫一個 View,然後讓系統來完成反序列化的工作,此時在 initWithCoder 呼叫之後,awakeFromNib 方法也會被執行,既然在 awakeFromNib 方法裡也能做初始化操作,那我們如何抉擇?

一般來說要儘量在 initWithCoder 中做初始化操作,畢竟這是最合理的地方,只要你的控制元件支援序列化,那麼它就能在任何被反序列化的時候執行初始化操作,這裡適合做全域性資料、狀態的初始化工作,也適合手動新增子檢視。

awakeFromNib 相較於 initWithCoder 的優勢是:當 awakeFromNib 執行的時候,各種 IBOutlet 也都連線好了;而 initWithCoder 呼叫的時候,雖然子檢視已經被新增到檢視層級中,但是還沒有引用。如果你是基於 xib 或 storyboard 建立的控制元件,那麼你可能需要對 IBOutlet 連線的子控制元件進行初始化工作,這種情況下,你只能在 awakeFromNib 裡進行處理。同時 xib 或 storyboard 對靈活性是有打折的,因為它們建立的程式碼無法被繼承,所以當你選擇用 xib 或 storyboard 來實現一個控制元件的時候,你已經不需要對靈活性有很高的要求了,唯一要做的是要保證使用者一定是通過 xib 建立的此控制元件,否則可能是一個空的檢視,可以在 initWithFrame 裡放置一個 斷言 或者異常來通知控制元件的使用者。

最後還要注意檢視層級的問題,比如你要給 View 放置一個背景,你可能會在 initWithCoderawakeFromNib 中這樣寫:

Objective-C
1 [self addSubview:self.backgroundView];// 通過懶載入一個背景 View,然後新增到檢視層級上

你的本意是在控制元件的最下面放置一個背景,卻有可能將這個背景覆蓋到控制元件的最上方,原因是使用者可能會在 xib 裡寫入這個控制元件,然後往它上面新增一些子檢視,這樣一來,使用者新增的這些子檢視會在你新增背景之前先進入檢視層級,你的背景被新增後就擋住了使用者的子檢視。如果你想支援使用者的這種操作,可以把 addSubview 替換成 insertSubview:atIndex:

同時支援從程式碼和檔案中載入

如果你要同時支援 initWithFrameinitWithCoder ,那麼你可以提供一個 commonInit 方法來做統一的初始化:

Objective-C
1234567891011121314151617181920 -(id)initWithCoder:(NSCoder*)aDecoder{self=[super initWithCoder:aDecoder];if(self){[selfcommonInit];}returnself;}-(id)initWithFrame:(CGRect)frame{self=[super initWithFrame:frame];if(self){[selfcommonInit];}returnself;}-(void)commonInit{// do something ...}

awakeFromNib 方法裡就不要再去呼叫 commonInit 了。

調整佈局的時機

當一個控制元件被初始化以及開始使用之後,它的 frame 仍然可能發生變化,我們也需要接受這些變化,因為你提供的是 UIView 的介面,UIView 有很多種初始化方式:initWithFrameinitWithCoderinit 和類方法 new,使用者完全可以在初始化之後再設定 frame 屬性,而且使用者就算使用 initWithFrame 來初始化也避免不了 frame 的改變,比如在橫豎屏切換的時候。為了確保當它的 Size 發生變化後其子檢視也能同步更新,我們不能一開始就把佈局寫死(使用約束除外)。

基於 frame

如果你是直接基於 frame 來佈局的,你應該確保在初始化的時候只新增檢視,而不去設定它們的frame,把設定子檢視 frame 的過程全部放到 layoutSubviews 方法裡:

Objective-C
123456789101112131415161718192021222324252627282930313233 -(instancetype)initWithCoder:(NSCoder*)aDecoder{self=[super initWithCoder:aDecoder];if(self){[selfcommonInit];}returnself;}-(instancetype)initWithFrame:(CGRect)frame{self=[super initWithFrame:frame];if(self){[selfcommonInit];}returnself;}-(void)layoutSubviews{[superlayoutSubviews];self.label.frame=CGRectInset(self.bounds,20,0);}-(void)commonInit{[self addSubview:self.label];}-(UILabel*)label{if(_label==nil){_label=[UILabelnew];_label.textColor=[UIColorgrayColor];}return_label;}

這麼做就能保證 label 總是出現在正確的位置上。

使用 layoutSubviews 方法有幾點需要注意:

  1. 不要依賴前一次的計算結果,應該總是根據當前最新值來計算
  2. 由於 layoutSubviews 方法是在自身的 bounds 發生改變的時候呼叫, 因此 UIScrollView 會在滾動時不停地呼叫,當你只關心 Size 有沒有變化的時候,可以把前一次的 Size 儲存起來,通過與最新的 Size 比較來判斷是否需要更新,在大多數情況下都能改善效能

基於 Auto Layout 約束

如果你是基於 Auto Layout 約束來進行佈局,那麼可以在 commonInit 呼叫的時候就把約束新增上去,不要重寫 layoutSubviews 方法,因為這種情況下它的預設實現就是根據約束來計算 frame。最重要的一點,把 translatesAutoresizingMaskIntoConstraints 屬性設為 NO,以免產生 NSAutoresizingMaskLayoutConstraint 約束,如果你使用 Masonry 框架的話,則不用擔心這個問題,mas_makeConstraints 方法會首先設定這個屬性為 NO:

Objective-C
12345678910 -(void)commonInit{...[selfsetupConstraintsForSubviews];}-(void)setupConstraintsForSubviews{[self.label mas_makeConstraints:^(MASConstraintMaker*make){...}];}

支援 sizeToFit

如果你的控制元件對尺寸有嚴格的限定,比如有一個統一的寬高比或者是固定尺寸,那麼最好能實現系統給出的約定成俗的介面。

sizeToFit 用在基於 frame 佈局的情況下,由你的控制元件去實現 sizeThatFits: 方法:

Objective-C
123456 -(CGSize)sizeThatFits:(CGSize)size{CGSizefitSize=[super sizeThatFits:size];fitSize.height+=self.label.frame.size.height;// 如果是固定尺寸,就像 UISwtich 那樣返回一個固定 Size 就 OK 了returnfitSize;}

然後在外部呼叫該控制元件的 sizeToFit 方法,這個方法內部會自動呼叫 sizeThatFits 並更新自身的 Size:

Objective-C
1 [self.customView sizeToFit];

在 ViewController 裡調整檢視佈局

當執行 viewDidLoad 方法時,不要依賴 self.view 的 Size。很多人會這樣寫:

Objective-C
1234 -(void)viewDidLoad{...self.label.width=self.view.width;}

這樣是不對的,哪怕看上去沒問題也只是碰巧沒問題而已。當 viewDidLoad 方法被呼叫的時候,self.view 才剛剛被初始化,此時它的容器還沒有對它的 frame 進行設定,如果 view 是從 xib 載入的,那麼它的 Size 就是 xib 中設定的值;如果它是從程式碼載入的,那麼它的 Size 和螢幕大小有關係,除了 Size 以外,Origin 也不會準確。整個過程看起來像這樣:

當訪問 ViewController 的 view 的時候,ViewController 會先執行 loadViewIfRequired 方法,如果 view 還沒有載入,則呼叫 loadView,然後是 viewDidLoad 這個鉤子方法,最後是返回 view,容器拿到 view 後,根據自身的屬性(如 edgesForExtendedLayout、判斷是否存在 tabBar、判斷 navigationBar 是否透明等)新增約束或者設定 frame。

你至少應該設定 autoresizingMask 屬性:

Objective-C
12345 -(void)viewDidLoad{

相關推薦

造輪子 | 怎樣設計一個面向協議的 iOS 網絡請求庫

結果 格式 object iscroll att main rac hide hud 近期開源了一個面向協議設計的網絡請求庫 MBNetwork,基於 Alamofire 和 ObjectMapper 實現,目的是簡化業務層的網絡請求操作。 須要幹

如何設計一個面向協議的 iOS 網路請求庫

最近開源了一個面向協議設計的網路請求庫 MBNetwork,基於 Alamofire 和 ObjectMapper 實現,目的是簡化業務層的網路請求操作。 需要幹些啥 對於大部分 App 而言,業務層做一次網路請求通常關心的問題有如下幾個: 如何在任

如何設計一個 iOS 控制元件? iOS 控制元件完全解析

程式碼的等級:可編譯、可執行、可測試、可讀、可維護、可複用 前言 一個控制元件從外在特徵來說,主要是封裝這幾點: 互動方式 顯示樣式 資料使用 對外在特徵的封裝,能讓我們在多種環境下達到 PM 對產品的要求,並且提到程式碼複用率,使維護工作保持在一個相對較小的範圍內;而

如何設計一個 iOS 控制元件?(iOS 控制元件完全解析)

程式碼的等級:可編譯、可執行、可測試、可讀、可維護、可複用 前言 一個控制元件從外在特徵來說,主要是封裝這幾點: 互動方式 顯示樣式 資料使用 對外在特徵的封裝,能讓我們在多種環境下達到 PM 對產品的要求,並

IOS版App的控制元件元素定位

前言 Android版App的控制元件元素可以通過Android studio自帶的工具uiautomatorviewer來協助定位! IOS版App的控制元件元素可以通過Appium來實現(未實現),或app-inspector來實現,在此記錄app-inspector的使用 安裝 一、安裝Node

iOS-自定義的控制元件UILabel、、、touches等系列事件不執行問題

本文首發地址 解決答案在最下面··· 1.場景描述場景描述 我繼承了UILabel搞了一個自定義的控制元件。 在搞上一些觸控事件 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)e

iOS UIView及其子控制元件的常用設定

為控制元件新增邊框 view.layer.borderWidth = 1; view.layer.borderColor = [[UIColor blackColor] CGColor]; b

ios-swift-為圖片控制元件(自定義控制元件)新增點選事件

@IBOutlet var img_guanggao: UIImageView! override func viewDidLoad() { super.viewDidLoad(

android仿ios的時間滾動控制元件WheelView

<LinearLayout android:layout_width="200dp" android:orientation="horizontal" android:layout_gravity=

android仿ios實現分段選擇控制元件UISegmentedControl

在ios7中有一種扁平風格的控制元件叫做分段選擇控制元件UISegmentedControl,控制元件上橫放或豎放著幾個被簡單線條隔開的按鈕,每次點選能切換不同的按鈕和按鈕所對應的介面,比如qq客戶端V6.5.3版本中訊息頁與電話頁分離就是用的這種原理。但是很可

iOS-Swift-MJRefresh 重寫控制元件

GTMRefresh GTMRefresh 用Swift重寫的MJRefresh Introduction 自定義方便, Demo裡面有國內主流App的下拉效果的模仿 程式碼簡潔,總程式碼量不超過1000行 支援國際化 支援: UITab

iOS開發——解決UIScrollView控制元件移動錯位和無法滾動

跟著iOS老師發的教程做了個簡陋的圖片瀏覽器,因為圖片太多展示不開所以想要用Scroll View來顯示,就自己試了一下。然而並沒有自己想象的那麼順利orz 當把要顯示的圖片都放置到Scroll View上時,它還是不會滾動。這時因為沒有設定他的content

[ ios ] 摺疊顯示文字控制元件

使用第三方控制元件"ZybTextView",可以方便的摺疊顯示大量文字。只需呼叫初始化方法,指定要顯示的文字的標題陣列和內容陣列。 使用方法,1.匯入標頭檔案,2.呼叫初始化方法。程式碼與效果如下

習題9:設計一個Windows應用程式,窗體上有一個TextBox控制元件一個Button控制元件

設計一個Windows應用程式,窗體上有一個TextBox控制元件、一個Button控制元件。要求,每當使用者單擊按鈕時,文字框都會增加一行文字來反映單擊的次數,例如“第3次單擊按鈕”。 【解答】 1) 窗體介面如圖Ex5-5-1所示; 2) 窗體中主要控制元件屬性設

iOS銀聯ApplePay控制元件開發

自從今早上蘋果準備向大陸開發Apple Pay,朋友圈以及各種QQ群裡就開始炸開了鍋,而且據說有幾個比較前衛的公司已經開始支援了Apple Pay,所以呢,我們的產品老大也閒不住了,加上自己還是比較感興趣的,於是乎,自己開始東西寫Demo了,就當趕個潮流吧。

iOS系統自帶控制元件 UIBarButtonSystemItem 的樣式解析

UIBarButtonSystemItem的樣式解析 樣式 圖片 UIBarButtonSystemItemDone UIBarButtonSystemItemCancel UIBarButton

ios 通過設定UI控制元件的center和size來設定位置時需注意!

<iframe id="iframeu848856_0" src="http://pos.baidu.com/mccm?rdid=848856&amp;dc=2&amp;di=u848856&amp;dri=0&amp;dis=0&amp;dai=2&

iOS 自定義重新整理控制元件UIScrollView (Refresh)

前言: 開發的時候經常會用到下拉重新整理這個控制元件,一直以來想自己寫一個,但是時間問題,都是使用別人寫好的,今天查了資料,自己自定一個 1.主要原理:        a.建立UIScrollView的類目 提供 類似addHeaderRefresh等方法,這樣tabl

iOS masonry動態約束控制元件位置

#import "FourViewController.h" #import "View+MASAdditions.h" #import "FiveViewControllerr.h" #define WS(weakSelf)  __weak __typeof(&

iOS開發-UI控制元件:UISwitch控制元件兩種使用方法和監聽

文章轉自: http://blog.csdn.net/totogo2010/article/details/7665815 一、第一種建立UISwitch控制元件的方法,在程式碼中動態建立。 1、開啟Xcode  4.3.2, 新建專案Switch,選擇Single Vi