IOS UIView 04- 自訂控制項

來源:互聯網
上載者:User

標籤:

註:本人是翻譯過來,並且加上本人的一點見解。

前言

  本文將討論一些自訂視圖、控制項的訣竅和技巧。我們先概述一下 UIKit 向我們提供的控制項,並介紹一些渲染技巧。隨後我們會深入到視圖和其所有者之間的通訊策略,並簡略探討協助工具功能,本地化和測試。

目錄

1.視圖層次概覽
2. 渲染
3. 自訂繪製
4. 自訂互動
5. 使用 Target-Action
6. 使用代理
7. 使用 Block
8. 使用 KVO
9. 使用通知
10. 協助工具功能 (Accessibility)
11. 本地化
12. 測試

1.視圖層次概覽

  如果你觀察一下 UIView 的子類,可以發現 3 個基類: reponders (響應者),views (視圖)和 controls (控制項)。我們快速重溫一下它們之間發生了什麼。

1) UIResponder

  UIResponderUIView 的父類。responder 能夠處理觸摸、手勢、遠端控制等事件。之所以它是一個單獨的類而沒有合并到UIView 中,是因為 UIResponder 有更多的子類,最明顯的就是 UIApplicationUIViewController。通過重寫 UIResponder的方法,可以決定一個類是否可以成為第一響應者 (first responder),即當前輸入焦點元素。

  當 touches (觸摸) 或 motion (指一系列動作感應器) 等互動行為發生時,它們被發送給第一響應者 (通常是一個視圖)。如果第一響應者沒有處理,則該行為沿著響應鏈到達視圖控制器UIViewController,如果行為仍然沒有被處理,則繼續傳遞給應用。如果想監測晃動手勢,可以根據需要在這3層中的任意位置處理。

  UIResponder 還允許自訂輸入方法,從 inputAccessoryView 向鍵盤添加輔助視圖到使用 inputView 提供一個完全自訂的鍵盤。

2) UIView

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

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

如果 transform 屬性不是 identity transform 的話,那麼這個屬性的值是未定義的,因此應該將其忽略

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

3) UIControl

  UIControl 建立在視圖上,增加了更多的互動支援。最重要的是,它增加了 target / action 模式。看一下具體的子類,我們可以看一下按鈕,日期選取器 (Date pickers),文字框等等。建立互動控制項時,你通常想要子類化一個 UIControl。一些常見的像 bar buttons (雖然也支援 target / action) 和 text view (這裡需要你使用代理來獲得通知) 的類其實並不是 UIControl

 

2. 渲染

  現在,我們轉向可見部分:自訂渲染。正如 Daniel 在他的文章中提到的,你可能想避免在 CPU 上做渲染而將其丟給 GPU。這裡有一條經驗:盡量避免 drawRect:,使用現有的視圖構建自訂視圖。

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

  為了實現這個,我們用以下的代碼建立了一個圖片視圖的子類:

// 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 上完成。

 

3. 自訂繪製

  如果決定了採用自訂繪製,有幾種不同的選項可供選擇。如果可能的話,看看是否可以產生一張圖片並在記憶體和磁碟上緩衝起來。如果內容是動態,也許你可以使用 Core Animation,如果還是行不通,使用 Core Graphics。如果你真的想要接近底層,使用 GLKit 和原生 OpenGL 也不是那麼難,但是需要做很多工作。

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

 

4. 自訂互動

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

[self sendActionsForControlEvents:UIControlEventValueChanged];

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

  建立自訂控制項時所面對的一個普遍的設計問題是向擁有它們的類中回傳傳回值。比如,假設你建立了一個繪製互動餅狀圖的自訂控制項,想知道使用者何時選擇了其中一個部分。你可以用很多種不同的方法來解決這個問題,比如通過 target action 模式,代理,block 或者 KVO,甚至通知。

 

5. 使用 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);}

  這麼做的好處是在自訂視圖子類中需要做的事情很少,並且自動獲得多目標支援。

 

6. 使用代理

  如果你需要更多的控制從視圖發送到視圖控制器的訊息,通常使用代理模式。在我們的餅狀圖中,代碼看起來大概是這樣:

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

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

@interface MyViewController <PieChartDelegate> ...- (void)setupPieChart{    self.pieChart.delegate = self;}- (void)pieChart:(PieChart*)pieChart didSelectSector:(PieChartSector*)sector{    // 處理區塊}

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

 

7. 使用 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 中的代碼過多),你還應該將它們抽離成獨立的方法,這種情況的話可能用代理會更好一些。

 

8. 使用 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

 

9. 使用通知

  作為最後一個選擇,如果你想要一個非常鬆散的耦合,可以使用通知來使其他對象得知變化。對於餅狀圖來說你幾乎肯定不想這樣,不過為了講解的完整,這裡介紹如何去做。在餅狀圖的的標頭檔中:

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 通知,你也需要在恰當的地方退訂這些通知。

  這項技術的好處是完全的解耦。另一方面,你失去了型別安全,因為在回調中你得到的是一個通知對象,而不像代理,編譯器無法檢查通知寄件者和接受者之間的類型是否匹配。

 

10. 協助工具功能 (Accessibility)

  蘋果官方提供的標準 iOS 控制項均有協助工具功能。這也是推薦用標準控制項建立自訂控制項的另一個原因。

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

 

11. 本地化

  建立自訂視圖時,本地化也同樣重要。像協助工具功能一樣,這個可以作為一整期的話題。本地化自訂視圖的最直接工作就是字串內容。如果使用 NSString,你不必擔心編碼問題。如果在自訂視圖中展示日期或數字,使用日期和數字格式化類來展示它們。使用 NSLocalizedString 本地化字串。

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

 

12. 測試

  最後,讓我們考慮測試檢視的問題。對於單元測試,你可以使用 Xcode 內建的工具或者其它第三方架構。另外,可以使用 UIAutomation 或者其它基於它的工具。為此,你的視圖完全支援協助工具功能是必要的。UIAutomation 並未充分得到利用的一個功能是;你可以用它自動對比視圖和設計以確保兩者每一個像素都分毫不差。(插一個無關的小提示:你還可以使用它來為應用上架 App Store 自動產生,這在你有多個多國語言的應用時會特別有用)。

IOS UIView 04- 自訂控制項

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.