本文將討論一些自定義視圖讯私、控件的訣竅和技巧热押。我們先概述一下 UIKit 向我們提供的控件,并介紹一些渲染技巧斤寇。隨后我們會深入到視圖和其所有者之間的通信策略桶癣,并簡略探討輔助功能,本地化和測試娘锁。
視圖層次概覽
如果你觀察一下 UIView 的子類牙寞,可以發(fā)現(xiàn) 3 個基類: reponders (響應者),views (視圖)和 controls (控件)莫秆。我們快速重溫一下它們之間發(fā)生了什么碎税。
UIResponder
UIResponder 是 UIView 的父類。responder 能夠處理觸摸馏锡、手勢雷蹂、遠程控制等事件。之所以它是一個單獨的類而沒有合并到UIView 中杯道,是因為 UIResponder 有更多的子類匪煌,最明顯的就是 UIApplication 和 UIViewController责蝠。通過重寫 UIResponder 的方法,可以決定一個類是否可以成為第一響應者 (first responder)萎庭,例如當前輸入焦點元素霜医。
當 touches (觸摸) 或 motion (指一系列運動傳感器) 等交互行為發(fā)生時,它們被發(fā)送給第一響應者 (通常是一個視圖)驳规。如果第一響應者沒有處理肴敛,則該行為沿著響應鏈到達視圖控制器,如果行為仍然沒有被處理吗购,則繼續(xù)傳遞給應用医男。如果想監(jiān)測晃動手勢,可以根據(jù)需要在這3層中的任意位置處理捻勉。
UIResponder 還允許自定義輸入方法镀梭,從 inputAccessoryView 向鍵盤添加輔助視圖到使用 inputView 提供一個完全自定義的鍵盤。
UIView
UIView 子類處理所有跟內(nèi)容繪制有關的事情以及觸摸時間踱启。只要寫過 "Hello, World" 應用的人都知道視圖报账,但我們重申一些技巧點:
一個普遍錯誤的概念:視圖的區(qū)域是由它的 frame 定義的。實際上 frame 是一個派生屬性埠偿,是由 center 和 bounds 合成而來透罢。不使用 Auto Layout 時,大多數(shù)人使用 frame 來改變視圖的位置和大小冠蒋。小心些羽圃,官方文檔特別詳細說明了一個注意事項:
如果 transform 屬性不是 identity transform 的話,那么這個屬性的值是未定義的浊服,因此應該將其忽略
另一個允許向視圖添加交互的方法是使用手勢識別统屈。注意它們對 responders 并不起作用胚吁,而只對視圖及其子類奏效牙躺。
UIControl
UIControl 建立在視圖上,增加了更多的交互支持腕扶。最重要的是孽拷,它增加了 target / action 模式“氡В看一下具體的子類脓恕,我們可以看一下按鈕,日期選擇器 (Date pickers)窿侈,文本框等等炼幔。創(chuàng)建交互控件時,你通常想要子類化一個 UIControl史简。一些常見的像 bar buttons (雖然也支持 target / action) 和 text view (這里需要你使用代理來獲得通知) 的類其實并不是 UIControl乃秀。
渲染
現(xiàn)在,我們轉(zhuǎn)向可見部分:自定義渲染。你可能想避免在 CPU 上做渲染而將其丟給 GPU跺讯。這里有一條經(jīng)驗:盡量避免 drawRect:枢贿,使用現(xiàn)有的視圖構(gòu)建自定義視圖。
通常最快速的渲染方法是使用圖片視圖刀脏。例如局荚,假設你想畫一個帶有邊框的圓形頭像,像下面圖片中這樣:
為了實現(xiàn)這個愈污,我們用以下的代碼創(chuàng)建了一個圖片視圖的子類:
// called from initializer
- (void)setupView
{
self.clipsToBounds = YES;
self.layer.cornerRadius = self.bounds.size.width / 2;
self.layer.borderWidth = 3;
self.layer.borderColor = [UIColor darkGrayColor].CGColor;
}
鼓勵各位讀者深入了解 CALayer 及其屬性耀态,因為你用它能實現(xiàn)的大多數(shù)事情會比用 Core Graphics 自己畫要快。然而一如既往钙畔,監(jiān)測自己的代碼的性能是十分重要的茫陆。
把可拉伸的圖片和圖片視圖一起使用也可以極大的提高效率。在 Taming UIButton 這個帖子中擎析,Reda Lemeden 探索了幾種不同的繪圖方法簿盅。在文章結(jié)尾處有一個很有價值的來自 UIKit 團隊的工程師 Andy Matuschak 的回復,解釋了可拉伸圖片是這些技術(shù)中最快的揍魂。原因是可拉伸圖片在 CPU 和 GPU 之間的數(shù)據(jù)轉(zhuǎn)移量最小桨醋,并且這些圖片的繪制是經(jīng)過高度優(yōu)化的。
處理圖片時现斋,你也可以讓 GPU 為你工作來代替使用 Core Graphics喜最。使用 Core Image,你不必用 CPU 做任何的工作就可以在圖片上建立復雜的效果庄蹋。你可以直接在 OpenGL 上下文上直接渲染瞬内,所有的工作都在 GPU 上完成。
自定義繪制
如果決定了采用自定義繪制限书,有幾種不同的選項可供選擇虫蝶。如果可能的話,看看是否可以生成一張圖片并在內(nèi)存和磁盤上緩存起來倦西。如果內(nèi)容是動態(tài)的能真,也許你可以使用 Core Animation,如果還是行不通扰柠,使用 Core Graphics粉铐。如果你真的想要接近底層,使用 GLKit 和原生 OpenGL 也不是那么難卤档,但是需要做很多工作蝙泼。
如果你真的選擇了重寫 drawRect:,確保檢查內(nèi)容模式劝枣。默認的模式是將內(nèi)容縮放以填充視圖的范圍汤踏,這在當視圖的 frame 改變時并不會重新繪制倡缠。
自定義交互
正如之前所說的,自定義控件的時候茎活,你幾乎一定會擴展一個 UIControl 的子類昙沦。在你的子類里,可以使用 target action 機制觸發(fā)事件载荔,如下面的例子:
[self sendActionsForControlEvents:UIControlEventValueChanged];
為了響應觸摸盾饮,你可能更傾向于使用手勢識別。然而如果想要更接近底層懒熙,仍然可以重寫 touchesBegan丘损, touchesMoved 和 touchesEnded 方法來訪問原始的觸摸行為。但雖說如此工扎,創(chuàng)建一個手勢識別的子類來把手勢處理相關的邏輯從你的視圖或者視圖控制器中分離出來徘钥,在很多情況下都是一種更合適的方式。
創(chuàng)建自定義控件時所面對的一個普遍的設計問題是向擁有它們的類中回傳返回值肢娘。比如呈础,假設你創(chuàng)建了一個繪制交互餅狀圖的自定義控件,想知道用戶何時選擇了其中一個部分橱健。你可以用很多種不同的方法來解決這個問題而钞,比如通過 target action 模式,代理拘荡,block 或者 KVO臼节,甚至通知。
使用 Target-Action
經(jīng)典學院派的珊皿,通常也是最方便的做法是使用 target-action网缝。在用戶選擇后你可以在自定義的視圖中做類似這樣的事情:
[self sendActionsForControlEvents:UIControlEventValueChanged];
如果有一個視圖控制器在管理這個視圖,需要這樣做:
- (void)setupPieChart
{
[self.pieChart addTarget:self
action:@selector(updateSelection:)
forControlEvents:UIControlEventValueChanged];
}
- (void)updateSelection:(id)sender
{
NSLog(@"%@", self.pieChart.selectedSector);
}
這么做的好處是在自定義視圖子類中需要做的事情很少蟋定,并且自動獲得多目標支持粉臊。
使用代理
如果你需要更多的控制從視圖發(fā)送到視圖控制器的消息,通常使用代理模式溢吻。在我們的餅狀圖中维费,代碼看起來大概是這樣:
[self.delegate pieChart:self didSelectSector:self.selectedSector];
在視圖控制器中果元,你要寫如下代碼:
@interface MyViewController <PieChartDelegate>
...
- (void)setupPieChart
{
self.pieChart.delegate = self;
}
- (void)pieChart:(PieChart*)pieChart didSelectSector:(PieChartSector*)sector
{
// 處理區(qū)塊
}
當你想要做更多復雜的工作而不僅僅是通知所有者值發(fā)生了變化時促王,這么做顯然更合適。不過雖然大多數(shù)開發(fā)人員可以非扯梗快速的實現(xiàn)自定義代理蝇狼,但這種方式仍然有一些缺點:你必須檢查代理是否實現(xiàn)了你想要調(diào)用的方法 (使用 respondsToSelector:),最重要的倡怎,通常你只有一個代理 (或者需要創(chuàng)建一個代理數(shù)組)迅耘。也就是說贱枣,一旦視圖所有者和視圖之間的通信變得稍微復雜,我們幾乎總是會采取這種模式颤专。
使用 Block
另一個選擇是使用 block纽哥。再一次用餅狀圖舉例,代碼看起來大概是這樣:
@interface PieChart : UIControl
@property (nonatomic,copy) void(^selectionHandler)(PieChartSection* selectedSection);
@end
在選取行為的代碼中栖秕,你只需要執(zhí)行它春塌。在此之前檢查一下block是否被賦值非常重要,因為執(zhí)行一個未被賦值的 block 會使程序崩潰簇捍。
if (self.selectionHandler != NULL) {
self.selectionHandler(self.selectedSection);
}
這種方法的好處是可以把相關的代碼整合在視圖控制器中:
- (void)setupPieChart
{
self.pieChart.selectionHandler = ^(PieChartSection* section) {
// 處理區(qū)塊
}
}
就像代理只壳,每個動作通常只有一個 block。另一個重要的限制是不要形成引用循環(huán)暑塑。如果你的視圖控制器持有餅狀圖的強引用吼句,餅狀圖持有 block,block 又持有視圖控制器事格,就形成了一個引用循環(huán)惕艳。只要在 block 中引用 self 就會造成這個錯誤。所以通常代碼會寫成這個樣子:
__weak id weakSelf = self;
self.pieChart.selectionHandler = ^(PieChartSection* section) {
MyViewController* strongSelf = weakSelf;
[strongSelf handleSectionChange:section];
}
一旦 block 中的代碼要失去控制 (比如 block 中要處理的事情太多驹愚,導致 block 中的代碼過多)尔艇,你還應該將它們抽離成獨立的方法,這種情況的話可能用代理會更好一些么鹤。
使用 KVO
如果喜歡 KVO终娃,你也可以用它來觀察。這有一點神奇而且沒那么直接蒸甜,但當應用中已經(jīng)使用棠耕,它是很好的解耦設計模式。在餅狀圖類中柠新,編寫代碼:
self.selectedSegment = theNewSelectedSegment;
當使用合成屬性窍荧,KVO 會攔截到該變化并發(fā)出通知。在視圖控制器中恨憎,編寫類似的代碼:
- (void)setupPieChart
{
[self.pieChart addObserver:self forKeyPath:@"selectedSegment" options:0 context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if(object == self.pieChart && [keyPath isEqualToString:@"selectedSegment"]) {
// 處理改變
}
}
根據(jù)你的需要蕊退,在 viewWillDisappear: 或 dealloc 中,還需要移除觀察者憔恳。對同一個對象設置多個觀察者很容易造成混亂瓤荔。有一些技術(shù)可以解決這個問題,比如ReactiveCocoa 或者更輕量級的THObserversAndBinders钥组。
使用通知
作為最后一個選擇输硝,如果你想要一個非常松散的耦合,可以使用通知來使其他對象得知變化程梦。對于餅狀圖來說你幾乎肯定不想這樣点把,不過為了講解的完整橘荠,這里介紹如何去做。在餅狀圖的的頭文件中:
extern NSString* const SelectedSegmentChangedNotification;
在實現(xiàn)文件中:
NSString* const SelectedSegmentChangedNotification = @"selectedSegmentChangedNotification";
- (void)notifyAboutChanges
{
[[NSNotificationCenter defaultCenter] postNotificationName:SelectedSegmentChangedNotification object:self];
}
現(xiàn)在訂閱通知郎逃,在視圖控制器中:
- (void)setupPieChart
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(segmentChanged:)
name:SelectedSegmentChangedNotification
object:self.pieChart];
}
...
- (void)segmentChanged:(NSNotification*)note
{
}
當添加了觀察者哥童,你可以不將餅狀圖作為參數(shù) object,而是傳遞 nil褒翰,以接收所有餅狀圖對象發(fā)出的通知如蚜。就像 KVO 通知,你也需要在恰當?shù)牡胤酵擞嗊@些通知影暴。
這項技術(shù)的好處是完全的解耦错邦。另一方面,你失去了類型安全型宙,因為在回調(diào)中你得到的是一個通知對象撬呢,而不像代理,編譯器無法檢查通知發(fā)送者和接受者之間的類型是否匹配妆兑。
輔助功能 (Accessibility)
蘋果官方提供的標準 iOS 控件均有輔助功能魂拦。這也是推薦用標準控件創(chuàng)建自定義控件的另一個原因。這或許可以作為一整期的主題搁嗓,但是如果你想編寫自定義視圖芯勘,Accessibility Programming Guide 說明了如何創(chuàng)建輔助控制器。最為值得注意的是腺逛,如果有一個視圖中有多個需要輔助功能的元素荷愕,但它們并不是該視圖的子視圖,你可以讓視圖實現(xiàn) UIAccessibilityContainer 協(xié)議棍矛。對于每一個元素安疗,返回一個描述它的 UIAccessibilityElement 對象。
本地化
創(chuàng)建自定義視圖時够委,本地化也同樣重要荐类。像輔助功能一樣,這個可以作為一整期的話題茁帽。本地化自定義視圖的最直接工作就是字符串內(nèi)容玉罐。如果使用 NSString,你不必擔心編碼問題潘拨。如果在自定義視圖中展示日期或數(shù)字吊输,使用日期和數(shù)字格式化類來展示它們。使用 NSLocalizedString 本地化字符串战秋。
另一個本地化過程中很有用的工具是 Auto Layout璧亚。例如讨韭,有在英文中很短的詞在德語中可能會很長脂信。如果根據(jù)英文單詞的長度對視圖的尺寸做硬編碼癣蟋,那么當翻譯成德文的時候幾乎一定會遇上麻煩。通過使用 Auto Layout狰闪,讓標簽控件自動調(diào)整為內(nèi)容的尺寸疯搅,并向依賴元素添加一些其他的限制以確保重新設置尺寸,使這項工作變得非常簡單埋泵。蘋果為此提供了一個很好的 介紹幔欧。另外,對于類似希伯來語這種順序從右到左的語言丽声,如果你使用了 leading 和 trailing 屬性礁蔗,整個視圖會自動按照從右到左的順序展示,而不是硬編碼的從左至右雁社。
測試
最后浴井,讓我們考慮測試視圖的問題。對于單元測試霉撵,你可以使用 Xcode 自帶的工具或者其它第三方框架磺浙。另外,可以使用 UIAutomation 或者其它基于它的工具徒坡。為此撕氧,你的視圖完全支持輔助功能是必要的喇完。UIAutomation 并未充分得到利用的一個功能是截圖锦溪;你可以用它自動對比視圖和設計以確保兩者每一個像素都分毫不差奄喂。(插一個無關的小提示:你還可以使用它來為應用上架 App Store 自動生成截圖海洼,這在你有多個多國語言的應用時會特別有用)。
此文章原文鏈接自己的個人博客: www.koalaliu.com ,因簡書平臺規(guī)范性以及用戶量坏逢,搬至簡書域帐。