自定義控件

本文將討論一些自定義視圖、控件的訣竅和技巧。咱們先概述一下 UIKit 向咱們提供的控件,並介紹一些渲染技巧。隨後咱們會深刻到視圖和其全部者之間的通訊策略,並簡略探討輔助功能,本地化和測試。html

視圖層次概覽

若是你觀察一下 UIView 的子類,能夠發現 3 個基類: reponders (響應者),views (視圖)和 controls (控件)。咱們快速重溫一下它們之間發生了什麼。ios

UIResponder

UIResponder 是 UIView 的父類。responder 可以處理觸摸、手勢、遠程控制等事件。之因此它是一個單獨的類而沒有合併到 UIView 中,是由於 UIResponder 有更多的子類,最明顯的就是 UIApplication 和 UIViewController。經過重寫 UIResponder 的方法,能夠決定一個類是否能夠成爲第一響應者 (first responder),例如當前輸入焦點元素。git

當 touches (觸摸) 或 motion (指一系列運動傳感器) 等交互行爲發生時,它們被髮送給第一響應者 (一般是一個視圖)。若是第一響應者沒有處理,則該行爲沿着響應鏈到達視圖控制器,若是行爲仍然沒有被處理,則繼續傳遞給應用。若是想監測晃動手勢,能夠根據須要在這3層中的任意位置處理。github

UIResponder 還容許自定義輸入方法,從 inputAccessoryView 向鍵盤添加輔助視圖到使用 inputView 提供一個徹底自定義的鍵盤。設計模式

UIView

UIView 子類處理全部跟內容繪製有關的事情以及觸摸時間。只要寫過 "Hello, World" 應用的人都知道視圖,但咱們重申一些技巧點:api

一個廣泛錯誤的概念:視圖的區域是由它的 frame 定義的。實際上 frame 是一個派生屬性,是由 center 和 bounds 合成而來。不使用 Auto Layout 時,大多數人使用 frame 來改變視圖的位置和大小。當心些,官方文檔特別詳細說明了一個注意事項:數組

若是 transform 屬性不是 identity transform 的話,那麼這個屬性的值是未定義的,所以應該將其忽略緩存

另外一個容許向視圖添加交互的方法是使用手勢識別。注意它們對 responders 並不起做用,而只對視圖及其子類奏效。安全

UIControl

UIControl 創建在視圖上,增長了更多的交互支持。最重要的是,它增長了 target / action 模式。看一下具體的子類,咱們能夠看一下按鈕,日期選擇器 (Date pickers),文本框等等。建立交互控件時,你一般想要子類化一個 UIControl。一些常見的像 bar buttons (雖然也支持 target / action) 和 text view (這裏須要你使用代理來得到通知) 的類其實並非 UIControlbash

渲染

如今,咱們轉向可見部分:自定義渲染。正如 Daniel 在他的文章中提到的,你可能想避免在 CPU 上作渲染而將其丟給 GPU。這裏有一條經驗:儘可能避免 drawRect:,使用現有的視圖構建自定義視圖。

一般最快速的渲染方法是使用圖片視圖。例如,假設你想畫一個帶有邊框的圓形頭像,像下面圖片中這樣:

Rounded image view

爲了實現這個,咱們用如下的代碼建立了一個圖片視圖的子類:

// 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 及其屬性,由於你用它能實現的大多數事情會比用 Core Graphics 本身畫要快。然而一如既往,監測本身的代碼的性能是十分重要的。

把可拉伸的圖片和圖片視圖一塊兒使用也能夠極大的提升效率。在 Taming UIButton 這個帖子中,Reda Lemeden 探索了幾種不一樣的繪圖方法。在文章結尾處有一個頗有價值的來自 UIKit 團隊的工程師 Andy Matuschak 的回覆,解釋了可拉伸圖片是這些技術中最快的。緣由是可拉伸圖片在 CPU 和 GPU 之間的數據轉移量最小,而且這些圖片的繪製是通過高度優化的。

處理圖片時,你也可讓 GPU 爲你工做來代替使用 Core Graphics。使用 Core Image,你沒必要用 CPU 作任何的工做就能夠在圖片上創建複雜的效果。你能夠直接在 OpenGL 上下文上直接渲染,全部的工做都在 GPU 上完成。

自定義繪製

若是決定了採用自定義繪製,有幾種不一樣的選項可供選擇。若是可能的話,看看是否能夠生成一張圖片並在內存和磁盤上緩存起來。若是內容是動態的,也許你可使用 Core Animation,若是仍是行不通,使用 Core Graphics。若是你真的想要接近底層,使用 GLKit 和原生 OpenGL 也不是那麼難,可是須要作不少工做。

若是你真的選擇了重寫 drawRect:,確保檢查內容模式。默認的模式是將內容縮放以填充視圖的範圍,這在當視圖的 frame 改變時並不會從新繪製。

自定義交互

正如以前所說的,自定義控件的時候,你幾乎必定會擴展一個 UIControl 的子類。在你的子類裏,可使用 target action 機制觸發事件,以下面的例子:

[self sendActionsForControlEvents:UIControlEventValueChanged];

複製代碼

爲了響應觸摸,你可能更傾向於使用手勢識別。然而若是想要更接近底層,仍然能夠重寫 touchesBegan, touchesMoved 和 touchesEnded 方法來訪問原始的觸摸行爲。但雖然說如此,建立一個手勢識別的子類來把手勢處理相關的邏輯從你的視圖或者視圖控制器中分離出來,在不少狀況下都是一種更合適的方式。

建立自定義控件時所面對的一個廣泛的設計問題是向擁有它們的類中回傳返回值。好比,假設你建立了一個繪製交互餅狀圖的自定義控件,想知道用戶什麼時候選擇了其中一個部分。你能夠用不少種不一樣的方法來解決這個問題,好比經過 target action 模式,代理,block 或者 KVO,甚至通知。

使用 Target-Action

經典學院派的,一般也是最方便的作法是使用 target-action。在用戶選擇後你能夠在自定義的視圖中作相似這樣的事情:

[self sendActionsForControlEvents:UIControlEventValueChanged];

複製代碼

若是有一個視圖控制器在管理這個視圖,須要這樣作:

- (void)setupPieChart
{
    [self.pieChart addTarget:self 
                  action:@selector(updateSelection:)
        forControlEvents:UIControlEventValueChanged];
}

- (void)updateSelection:(id)sender
{
    NSLog(@"%@", self.pieChart.selectedSector);
}

複製代碼

這麼作的好處是在自定義視圖子類中須要作的事情不多,而且自動得到多目標支持。

使用代理

若是你須要更多的控制從視圖發送到視圖控制器的消息,一般使用代理模式。在咱們的餅狀圖中,代碼看起來大概是這樣:

[self.delegate pieChart:self didSelectSector:self.selectedSector];

複製代碼

在視圖控制器中,你要寫以下代碼:

@interface MyViewController <PieChartDelegate>

 ...

- (void)setupPieChart
{
    self.pieChart.delegate = self;
}

- (void)pieChart:(PieChart*)pieChart didSelectSector:(PieChartSector*)sector
{
    // 處理區塊
}

複製代碼

當你想要作更多複雜的工做而不只僅是通知全部者值發生了變化時,這麼作顯然更合適。不過雖然大多數開發人員能夠很是快速的實現自定義代理,但這種方式仍然有一些缺點:你必須檢查代理是否實現了你想要調用的方法 (使用 respondsToSelector:),最重要的,一般你只有一個代理 (或者須要建立一個代理數組)。也就是說,一旦視圖全部者和視圖之間的通訊變得稍微複雜,咱們幾乎老是會採起這種模式。

使用 Block

另外一個選擇是使用 block。再一次用餅狀圖舉例,代碼看起來大概是這樣:

@interface PieChart : UIControl

@property (nonatomic,copy) void(^selectionHandler)(PieChartSection* selectedSection);

@end

複製代碼

在選取行爲的代碼中,你只須要執行它。在此以前檢查一下block是否被賦值很是重要,由於執行一個未被賦值的 block 會使程序崩潰。

if (self.selectionHandler != NULL) {
    self.selectionHandler(self.selectedSection);
}

複製代碼

這種方法的好處是能夠把相關的代碼整合在視圖控制器中:

- (void)setupPieChart
{
    self.pieChart.selectionHandler = ^(PieChartSection* section) {
        // 處理區塊
    }
}

複製代碼

就像代理,每一個動做一般只有一個 block。另外一個重要的限制是不要造成引用循環。若是你的視圖控制器持有餅狀圖的強引用,餅狀圖持有 block,block 又持有視圖控制器,就造成了一個引用循環。只要在 block 中引用 self 就會形成這個錯誤。因此一般代碼會寫成這個樣子:

__weak id weakSelf = self;
self.pieChart.selectionHandler = ^(PieChartSection* section) {
    MyViewController* strongSelf = weakSelf;
    [strongSelf handleSectionChange:section];
}

複製代碼

一旦 block 中的代碼要失去控制 (好比 block 中要處理的事情太多,致使 block 中的代碼過多),你還應該將它們抽離成獨立的方法,這種狀況的話可能用代理會更好一些。

使用 KVO

若是喜歡 KVO,你也能夠用它來觀察。這有一點神奇並且沒那麼直接,但當應用中已經使用,它是很好的解耦設計模式。在餅狀圖類中,編寫代碼:

self.selectedSegment = theNewSelectedSegment;

複製代碼

當使用合成屬性,KVO 會攔截到該變化併發出通知。在視圖控制器中,編寫相似的代碼:

- (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"]) {
        // 處理改變
    }
}

複製代碼

根據你的須要,在 viewWillDisappear: 或 dealloc 中,還須要移除觀察者。對同一個對象設置多個觀察者很容易形成混亂。有一些技術能夠解決這個問題,好比 ReactiveCocoa 或者更輕量級的 THObserversAndBinders

使用通知

做爲最後一個選擇,若是你想要一個很是鬆散的耦合,可使用通知來使其餘對象得知變化。對於餅狀圖來講你幾乎確定不想這樣,不過爲了講解的完整,這裏介紹如何去作。在餅狀圖的的頭文件中:

extern NSString* const SelectedSegmentChangedNotification;

複製代碼

在實現文件中:

NSString* const SelectedSegmentChangedNotification = @"selectedSegmentChangedNotification";

...

- (void)notifyAboutChanges
{
    [[NSNotificationCenter defaultCenter] postNotificationName:SelectedSegmentChangedNotification object:self];
}

複製代碼

如今訂閱通知,在視圖控制器中:

- (void)setupPieChart
{
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                       selector:@selector(segmentChanged:) 
                                           name:SelectedSegmentChangedNotification
                                          object:self.pieChart];

}

...

- (void)segmentChanged:(NSNotification*)note
{
}

複製代碼

當添加了觀察者,你能夠不將餅狀圖做爲參數 object,而是傳遞 nil,以接收全部餅狀圖對象發出的通知。就像 KVO 通知,你也須要在恰當的地方退訂這些通知。

這項技術的好處是徹底的解耦。另外一方面,你失去了類型安全,由於在回調中你獲得的是一個通知對象,而不像代理,編譯器沒法檢查通知發送者和接受者之間的類型是否匹配。

輔助功能 (Accessibility)

蘋果官方提供的標準 iOS 控件均有輔助功能。這也是推薦用標準控件建立自定義控件的另外一個緣由。

這或許能夠做爲一整期的主題,可是若是你想編寫自定義視圖,Accessibility Programming Guide 說明了如何建立輔助控制器。最爲值得注意的是,若是有一個視圖中有多個須要輔助功能的元素,但它們並非該視圖的子視圖,你可讓視圖實現 UIAccessibilityContainer 協議。對於每個元素,返回一個描述它的 UIAccessibilityElement 對象。

本地化

建立自定義視圖時,本地化也一樣重要。像輔助功能同樣,這個能夠做爲一整期的話題。本地化自定義視圖的最直接工做就是字符串內容。若是使用 NSString,你沒必要擔憂編碼問題。若是在自定義視圖中展現日期或數字,使用日期和數字格式化類來展現它們。使用 NSLocalizedString 本地化字符串。

另外一個本地化過程當中頗有用的工具是 Auto Layout。例如,有在英文中很短的詞在德語中可能會很長。若是根據英文單詞的長度對視圖的尺寸作硬編碼,那麼當翻譯成德文的時候幾乎必定會趕上麻煩。經過使用 Auto Layout,讓標籤控件自動調整爲內容的尺寸,並向依賴元素添加一些其餘的限制以確保從新設置尺寸,使這項工做變得很是簡單。蘋果爲此提供了一個很好的 介紹。另外,對於相似希伯來語這種順序從右到左的語言,若是你使用了 leading 和 trailing 屬性,整個視圖會自動按照從右到左的順序展現,而不是硬編碼的從左至右。

測試

最後,讓咱們考慮測試視圖的問題。對於單元測試,你可使用 Xcode 自帶的工具或者其它第三方框架。另外,可使用 UIAutomation 或者其它基於它的工具。爲此,你的視圖徹底支持輔助功能是必要的。UIAutomation 並未充分獲得利用的一個功能是截圖;你能夠用它自動對比視圖和設計以確保二者每個像素都分絕不差。(插一個無關的小提示:你還可使用它來爲應用上架 App Store 自動生成截圖,這在你有多個多國語言的應用時會特別有用)。


原文 Custom Controls

相關文章
相關標籤/搜索