iOS設計模式之三:介面卡模式和觀察者模式

發表於2013-09-18

介面卡(Adapter)模式

介面卡可以讓一些介面不相容的類一起工作。它包裝一個物件然後暴漏一個標準的互動介面。

如果你熟悉介面卡設計模式,蘋果通過一個稍微不同的方式來實現它,蘋果使用了協議的方式來實現。你可能已經熟悉UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying協議。舉個例子,使用NSCopying協議,任何類都可以提供一個標準的copy方法。

如何使用介面卡模式

前面提到的水平滾動檢視如下圖所示:

為了開始實現它,在工程導航檢視中右鍵點選View組,選擇New File…使用iOS\Cocoa Touch\Objective-C class 模板建立一個類。命名這個新類為HorizontalScroller,並且設定它是UIView的子類。

開啟HorizontalScroller.h檔案,在@end 行後面插入如下程式碼:

上面的程式碼定義了一個名為HorizontalScrollerDelegate的協議,它採用Objective-C 類繼承父類的方式繼承自NSObject協議。去遵循NSObject協議或者遵循一個本身實現了NSObject協議的類 是一條最佳實踐,這使得你可以給HorizontalScroller的委託傳送NSObject定義的訊息。你不久會意識到為什麼這樣做是重要的。

在@protocol和@end之間,你定義了委託必須實現以及可選的方法。所以增加下面的方法:

這裡你既有必需的方法也有可選方法。必需的方法要求委託必須實現它,因為它提供一些必需的資料。在這裡,必需的是檢視的數量,指定索引位置的檢視,以及使用者點選檢視後的行為,可選的方法是初始化檢視;如果它沒有實現,那麼HorizontalScroller將預設用第一個索引的檢視。

下一步,你需要在HorizontalScroller類中引用新建的委託。但是委託的定義是在類的定義之後的,所以在類中它是不可見的,怎麼辦呢?

解決方案就是前置宣告委託協議以便編譯器(和Xcode)知道協議的存在。如何做?你只需要在@interface行前面增加下面的程式碼即可:

繼續在HorizontalScroller.h檔案中,在@interface 和@end之間增加如下的語句:

這裡你宣告屬性為weak.這樣做是為了防止迴圈引用。如果一個類強引用它的委託,它的委託也強引用那個類,那麼你的app將會出現記憶體洩露,因為任何一個類都不能釋放調分配給另一個類的記憶體。

id意味著delegate屬性可以用任何遵從HorizontalScrollerDelegate的類賦值,這樣可以保障一定的型別安全。

reload方法在UITableView的reloadData方法之後被呼叫,它重新載入所有的資料去構建水平滾動檢視。

用如下的程式碼取代HorizontalScroller.m的內容:

讓我們來對上面每個註釋塊的內容進行一一分析:

  • 1. 定義了一系列的常量以方便在設計的時候修改檢視的佈局。水平滾動檢視中的每個子檢視都將是100*100,10點的邊框的矩形.
  • 2. HorizontalScroller遵循UIScrollViewDelegate協議。因為HorizontalScroller使用UIScrollerView去滾動專輯封面,所以它需要使用者停止滾動類似的事件
  • 3.建立了UIScrollerView的例項。

下一步你需要實現初始化器。增加下面的程式碼:

滾動檢視完全充滿了HorizontalScroller。UITapGestureRecognizer檢測滾動檢視的觸控事件,它將檢測專輯封面是否被點選了。如果專輯封面被點選了,它會通知HorizontalScroller的委託。

現在,增加下面的程式碼:

Gesture物件被當做引數傳遞,讓你通過locationInView:匯出點選的位置。

接下來,你呼叫了numberOfViewsForHorizontalScroller:委託方法,HorizontalScroller例項除了知道它可以安全的傳送這個訊息給委託之外,它不知道其它關於委託的資訊,因為委託必須遵循HorizontalScrollerDelegate協議。

對於滾動檢視中的每個子檢視,通過CGRectContainsPoint方法發現被點選的檢視。當你已經找到了被點選的檢視,給委託傳送horizontalScroller:clickedViewAtIndex:訊息。在退出迴圈之前,將被點選的檢視放置到滾動檢視的中間。

現在增加下面的程式碼去重新載入滾動檢視:

我們來一步步的分析程式碼中有註釋的地方:

  • 1. 如果沒有委託,那麼不需要做任何事情,僅僅返回即可。
  • 2. 移除之前新增到滾動檢視的子檢視
  • 3. 所有的檢視的位置從給定的偏移量開始。當前的偏移量是100,它可以通過改變檔案頭部的#DEFINE來很容易的調整。
  • 4. HorizontalScroller每次從委託請求檢視物件,並且根據預先設定的邊框來水平的放置這些檢視。
  • 5. 一旦所有檢視都設定好了以後,設定UIScrollerView的內容偏移(contentOffset)以便使用者可以滾動的檢視所有的專輯封面。
  • 6. HorizontalScroller檢測是否委託實現了initialViewIndexForHorizontalScroller:方法,這個檢測是需要的,因為這個方法是可選的。如果委託沒有實現這個方法,0就是預設值。最後設定滾動檢視為協議規定的初始化檢視的中間。

當資料已經發生改變的時候,你要執行reload方法。當增加HorizontalScroller到另外一個檢視的時候,你也需要呼叫reload方法。增加下面的程式碼來實現後面一種場景:

didMoveToSuperview方法會在檢視被增加到另外一個檢視作為子檢視的時候呼叫,這正式重新載入滾動檢視的最佳時機。

最後我們需要確保所有你正在瀏覽的專輯資料總是在滾動檢視的中間。為了這樣做,當使用者的手指拖動滾動檢視的時候,你將需要做一些計算。

再一次在HorizontalScroller.m中增加如下方法:

為了計算當前檢視到中間的距離,上面的程式碼考慮了滾動檢視當前的偏移量,檢視的尺寸以及邊框。最後一行程式碼是重要的,一當子檢視被置中,你將需要將這種變化通知委託。

為了檢測使用者在滾動檢視中的滾動,你必需增加如下的UIScrollerViewDelegate方法:

scrollViewDidEndDragging:willDecelerate:方法在使用者完成拖動的時候通知委託。如果檢視還沒有完全的停止,那麼decelerate引數為true.當滾動完全停止的時候,系統將會呼叫scrollViewDidEndDecelerating.在兩種情況下,我們都需要呼叫我們新增的方法去置中當前的檢視,因為當前的檢視在使用者拖動以後可能已經發生了變化。

你的HorizontalScroller現在已經可以使用了。瀏覽你剛剛寫的程式碼,沒有涉及到任何與Album或AlbumView類的資訊。這個相對的棒,因為這意味著這個新的滾動檢視是完全的獨立和可複用的。

構建的工程確保每個資源可以正確編譯。

現在HorizontalScroller完整了,是時候去在app使用它了。開啟ViewController.m 增加下面的匯入語句:

增加HorizontalScrollerDelegate協議為ViewController遵循的協議:

在類的擴充套件中增加下面的例項變數:

HorizontalScroller *scroller;

現在你可以實現委託方法;你可能會感到驚訝,因為只需要幾行程式碼就可以實現大量的功能啦。

在ViewController.m中增加下面的程式碼:

它設定儲存當前專輯資料的變數,然後呼叫showDataForAlbumAtIndex:方法顯示專輯資料。

注意:在#pragma mark 指令後面寫方法程式碼是一種通用的實踐。c 編譯器會忽略調這些行,但是如果你通過Xcode的彈出框的時候,你將看到這些指令會幫你把程式碼組織成有獨立和粗體標題的組。這可以幫你使得你的程式碼更方便在Xcode中導航。

接下來,增加下面的程式碼:

正如你意識到的,這個是返回滾動檢視所有子檢視數量的協議方法。因為滾動檢視要顯示所有專輯的封面,這個數量就是專輯記錄的數量。

現在,增加下面的程式碼:

這裡你建立了一個新的AlbumView,並且將它傳遞給HorizontalScroller。

夠了,僅僅三個簡短的方法就可以顯示一個漂亮的水平滾動檢視。

是的,你任然需要建立滾動檢視,並且把它增加到你的主檢視中,但是在這樣做之前,你增加下面的方法先:

這個方法通過LibraryAPI載入專輯資料,然後根據當前檢視的索引設定當前顯示的檢視。如果當前的檢視索引小於0,意味著當前沒有選定任何檢視,此時可以選擇第一個專輯來顯示,否則下面一個專輯將會顯示。

現在在viewDidLoad的[self showDataForAlbumAtIndex:0]之前增加下面的程式碼來初始化滾動檢視:

上面的程式碼簡單的建立了一個HorizontalScroller類的例項,設定它的背景色,委託,增加它到主檢視,然後載入所有子檢視去顯示專輯資料。

注意:如果一個協議變得特別冗長,包含太多的方法。你應該考慮將它氛圍更家細粒度的協議。UITableViewDelegate 和 UITableViewDataSource是一個好的例子。因為它們都是UITableView的協議。試著設計你的協議以便每個協議都關注特定的功能。

構建並執行你的on過程,檢視一下你帥氣十足的水平滾動檢視吧:

對了,等等。水平滾動檢視沒問題,但是為什麼沒有顯示封面呢?

是的,那就對了-你還沒有實現下載封面的程式碼。為了實現這個功能,你需要去新增一個下載圖片的方法。因為所有對服務的訪問都通過LibraryAPI,那我們就可以在LibraryAPI中實現新的方法。然而我們首先需要慮一些事情:

  • 1. AlbumView不應該直接和LibraryAPI互動。你不想混淆顯示邏輯和網路互動邏輯。
  • 2. 同樣的原因,LibraryAPI也不應該知道AlbumView。
  • 3. 一旦封面已經下載,LibraryAPI需要通知AlbumView,因為AlbumView顯示專輯封面。

聽上去是不是挺糊塗的?不要灰心。你將學習如何使用觀察者模式來實現它。

 

觀察者(Observer)模式

在觀察者模式中,一個物件任何狀態的變更都會通知另外的對改變感興趣的物件。這些物件之間不需要知道彼此的存在,這其實是一種鬆耦合的設計。當某個屬性變化的時候,我們通常使用這個模式去通知其它物件。

此模式的通用實現中,觀察者註冊自己感興趣的其它物件的狀態變更事件。當狀態發生變化的時候,所有的觀察者都會得到通知。蘋果的推送通知(Push Notification)就是一個此模式的例子。

如果你要遵從MVC模式的概念,你需要讓模型物件和檢視物件在不相互直接引用的情況下通訊。這正是觀察者模式的用武之地。

Cocoa通過通知(Notifications)和Key-Value Observing(KVO)來實現觀察者模式。

通知(Notifications)

不要和遠端推送以及本地通知所混淆,通知是一種基於訂閱-釋出模式的模型,它讓釋出者可以給訂閱者傳送訊息,並且釋出者不需要對訂閱者有任何的瞭解。

通知在蘋果官方被大量的使用。舉例來說,當鍵盤彈出或者隱藏的時候,系統會獨立傳送 UIKeyboardWillShowNotification/UIKeyboardWillHideNotification 通知。當你的應用進入後臺執行的時候,系統會傳送一個UIApplicationDidEnterBackgroundNotification 通知。

注意:開啟UIApplication.h,在檔案的末尾,你將看到一個由系統發出的超過20個通知組成的列表。

如何使用通知(Notifications)

開啟AlbumView.m,在initWithFrame:albumCover::方法的[self addSubview:indicator];語句之後加入如下程式碼:

這行程式碼通過NSNotificationCenter單例傳送了一個通知。這個通知包含了UIImageView和需要下載的封面URL,這些是你下載任務所需要的所有資訊。

在LibraryAPI.m檔案init方法的isOnline=NO之後,增加如下的程式碼:

這個是觀察者模式中兩部分的另外一部分:觀察者。每次AlbumView傳送一個BLDownloadImageNotification通知,因為LibraryAPI已經註冊為同樣的通知的觀察者,那麼系統就會通知LibraryAPI,LibraryAPI又會呼叫downloadImage:來響應。

然而在你實現downloadImage:方法之前,你必須在你的物件銷燬的時候,退訂所有之前訂閱的通知。如果你不能正確的退訂的話,一個通知傳送給一個已經銷燬的物件會導致你的app崩潰。

在Library.m中增加下面的程式碼:

當物件被銷燬的時候,它將移除所有監聽通知的觀察者。

還有一件事情需要去做,將已經下載的封面圖片本地儲存起來是個不錯的主意,這樣可以避免每次都重新下載相同的封面。

開啟PersistencyManager.h檔案,增加下面兩個方法原型:

在PersistencyManager.m檔案中,增加方法的實現:

上面的程式碼相當直接。下載的圖片會被儲存在文件(Documents)目錄,如果在文件目錄不存在指定的檔案,getImage:方法將返回nil.

現在在LibraryAPI.m中增加下面的方法:

下面是以上程式碼分段描述:

  • 1. downloadImage方法是通過通知被執行的,所以通知物件會當作引數傳遞。UIImageView和圖片URL都會從通知中獲取。
  • 2. 如果圖片已經被下載過了,直接從PersistencyManager方法獲取。
  • 3. 如果圖片還沒有被下載,通過HTTPClient去獲取它。
  • 4. 當圖片下載的時候,將它顯示在UIImageView中,同時使用PersistencyManager儲存到本地。

再一次,你使用了門面(Facade)模式隱藏了下載圖片的複雜性。通知的傳送者不需要關心圖片是來自網路還是來自本地檔案系統。

構建並執行你的應用,看看那些在滾動檢視中的漂亮封面吧:

停止你的應用再一次執行它,你會注意到不會存在載入圖片的延遲,因為它們都已經被儲存到了本地。甚至你可以斷開網路,你的應用也可以完美地執行。然而這裡有點奇怪,圖片上的提示轉盤一直在轉動,出了什麼問題呢?

當開始下載圖片的時候,你啟動了提示圖片正在載入的旋轉提示器,但是你還沒有實現圖片下載完成後停止它的邏輯。你應該在每次圖片下載完成的時候傳送一個通知,但是這裡你使用KVO這種觀察者模式。

 

Key-Value Observing(KVO)模式

在KVO中,一個物件可以要求在它自身或者其它物件的屬性傳送變化的時候得到通知。如果你對KVO感興趣的話,你可以更進一步的閱讀這篇文章:Apple’s KVO Programming Guide..

如何使用KVO

正如上面所說的,KVO機制讓物件可以感知到屬性的變化。在本例中,你可以使用KVO去觀察UIImageView的image屬性的變化。

開啟AlbumView.m檔案,在initWithFrame:albumCover:方法[self addSubview:indicator]這一行後,增加下面的程式碼:

這裡它增加了它自己(當前的類)作為image屬性的觀察者。當完成的時候,你同樣需要登出相應的觀察者。仍然在AlbumView.m中增加下面的程式碼:

最後增加下面的方法:

你必須在每個觀察者類中實現這個方法。系統會在被觀察的屬性傳送變化的時候通知觀察者。在上面的程式碼中,當image屬性變化的時候,你停止了封面上面的旋轉提示器。這樣以來,當圖片載入完後,旋轉提示器將會停止。

構建並執行的你的工程。旋轉提示器應該會消失:

注意:你要總是記得去移除已經銷燬的觀察者,否則當給不存在的觀察者傳送訊息的時候,你的應用可能會崩潰。

如果你玩一回你的應用後終止它,你會發現你的應用狀態沒有被儲存,你上次檢視的專輯不是下次啟動時候的預設專輯。

為了修正這個問題,你可以使用列表中的下個模式:備忘錄(Memento)模式.

相關文章