Ios 設計模式,你可能聽說過,但是你真正知道這是什么意思么?大部分的開發者大概都同意設計模式很重要,但是關於這一部分卻沒有很多的文章去介紹它,我們開發者很多時候寫代碼的時候也並不重視設計模式.
設計模式是在軟件設計上去解決普通問題的可重用的方法.他們是是幫助你讓所寫的代碼更加容易理解和提高可重用性的模板.它們還可以幫你創建松散耦合的代碼是你能不費很大功夫就可以改變或者替代你的代碼中的一部分.
如果你對設計模式感到生疏,那么我有個好消息告訴你!首先,你已經用了很多ios設計模式多虧了Cocoa 內建的方法。其次,這個教程會帶你加快對在Cocoa中最常用的ios設計模式的認識。
這個教程會分割成幾部分,每一部分都是一個設計模式,在每一個部分,你都會讀到以下內容:這是什么設計模式,為什么使用這個設計模式,怎么使用這個設計模式,當使用這個設計模式時要注意的常見的陷阱。
在這個教程中,你會創建一個音樂庫app-會顯示專輯和他們相應的信息。
在開發這款app的過程中,你將會熟悉以下Cocoa中最常見的設計模式。
1創建類型的:單例模式,抽象工廠模式
2結構化類型的:MVC, Decorator, Adapter, Facade and Composite
3行為類型的:Observer, Memento, Chain of Responsibility andCommand。
不要以為這個是關於理論的一篇文章,你將會學到如何使用這些設計模式的大部分去創建這款app。在這個教程結束后你的app將會是這個樣子:
開始:
下載開始工程,這只是默認的ViewController和一個簡單的包含空的實現文件的HTTP客戶端。
常識:你知道當你創建一個空的Xcode文件工程那你的代碼已經充滿了設計模式?MVC,Delegate,Protocol,Singleton。
在你涉足到第一個設計模式時,你要創建兩個類去hold和展示專輯的數據。
按住Command + N鍵去創建一個名為Album的object-c文件,繼承於NSObject。打開Album.h文件,加入以下的屬性和方法。
- @property (nonatomic, copy, readonly) NSString *title, *artist, *genre, *coverUrl, *year;
- - (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year;
注意到這些屬性是只讀的,因為當Album創建以后沒有必要去改變他們。
這個方法是這個對象的初始化方法,當你創建了一個專輯,你會傳給它專輯的名字,藝術家的名字,專輯封面的URL和專輯的年份。
打開Album.m然后將下面代碼添加到 @implementation和@end之間。
這里沒有什么神奇的; 只是一個簡單的init方法來創建新Album的實例。
再次,導航到文件\新建\文件...。選擇Cocoa Touch,然后Objective-C類,然后單擊下一步。將類名AlbumView,但這次設定的子類UIView的。單擊下一步,然后創建。
打開ALbumView.h文件,在imlementation.h中添加以下代碼:
- @imlementation AlbumView
- {
- UIImageView*coverImage;
- UIActivityIndicatorView*indicator;
- }
在這里你需要注意的第一件事情是有一個實例變量名為coverImage,它代表了專輯的封面圖片,第二個變量是indicator,它的旋轉去指示圖片正在被下載。
在實現部分的初始化中將背景設為黑色,創建的image view有5像素邊緣,然后將指示器indicator添加到試圖中。
小貼士:為什么將私有變量定義在實現部分而不是接口部分?這是因為外部的類不需要知道這些變量的存在因為它們只是在這個類中的實現部分被使用。這個約定是十分重要的,如果你在創建一個庫或者框架給其他開發者使用。
編譯你的工程(Comamand +B)確保沒有問題,接下來准備好去接受你的第一個設計模式吧。
MVC -設計模式之王
Model View Controller 是Cocoa的基石之一,且毫無疑問的是所有設計模式中最常用的設計模式,它根據你的應用中的一般角色去分類對象,鼓勵在完全分離的模式下分角色。
Model:這個對象hold住你的應用數據,且定義如何去操作它,例如本例中就是Album類。
View:這個對象掌管了Model的可視化顯示,和控制用戶的交互,基本上所有的都是UIView和它的子類。在本例中這個就是被分離成的AlbumView類。
Controller:控制器是調節所有工作的調節器,它訪問模型中的數據,然后用視圖去顯示它,根據要求監聽事件和操作數據。你能想象在這個哪個是Controller么,就是ViewController.
視圖和模型通過控制器去交流的場景可以被描述成以下圖:
如果在Model中有任何數據變化,那么它就會通知Controller,反過來,Controller更新在View中的數據,View可以通知Controller關於用戶的行為,然后Controller要么根據需要或者檢索要求的數據去更新Model。
你也許會懷疑為什么不只是創建了Controller然后將View和Model一起放到里面去實現?那樣看起來不是更容易么?
這所有的所有都是為了是代碼分離化和提高可重用性。理想情況下,視圖應該會從Model中完全分離出來,如果視圖不依賴於某個具體的Model的實現部分,那么它可以用不同的Model去展示其他一些數據來實現它的可重用性。
例如:如果將來你想添加一些電影和書籍到你的庫中去,你仍然可以使用相同的AlbumView去展示你的電影和書籍的對象,更進一步說,如果你想去創建一個工程去處理專輯,你可以很簡單的去重用你的Album類,因為它不依賴於任何一個視圖。這就是MVC的魔力。
如何實現MVC模式
首先,你需要確保你的工程中的每一個類都是Controller,或者View,或者Model,不要講任何兩個中的角色的任務連接在一塊,通過創建Album和AlbumV類你已經做了一個很好的工作。
其次,為了確保遵守這個工作方法,你應該創建三個工程組去hold住你的代碼,每類一個分組。
按住Command+option+N鍵,創建一個組,名為Model,同樣創建View和Controller,將Album.h和Album.m拖入Model中,拖動AlbumView.h和AlbumView.m的視圖組,最后拖ViewController.h和ViewController.m到控制器組.
這時候你的工程結構應該看起來是這樣的:
現在看起來沒有那些文件浮在四周,看着好多了。顯然你可以有其他的組和類,但是這個應用中的核心就是包含在這三個類中的。
既然你的組成部分已經被組織起來了,你需要從別的地方去獲得album的數據,你將會創建一個API類去在全部的代碼中去管理這些數據-這將會在你的下一個設計模式-單例中得到展示。
單例模式
單例模式確保為一個確定的類只有一個實例存在,而且有一個全局的訪問指針只想它,他經常使用延時加載去在第一次使用的時候創建一個簡單的實例。
小貼士:蘋果使用這個方法很頻繁。比如:[NSUserDefaults standarUserDefaults], [UIApplicationsharedApplication],[UIScreen mainScreen],[NSFileManager defaultManager],都返回一個單例。
你可能會以為為什么你會介意一個類周圍會有不止一個實例,代碼和內存都很簡化,對不對?
有一些情況下一個類只有一個實例會很有意義。比如:沒有必要去有很多的Logger實例,除非你想要同時寫入幾個日記文件,或者,擁有一個全聚德配置控制的類,很容易就實現一個線程安全的單個共享資源,比如配置文件,而不是多個類同時修改同一個配置文件。
如何使用單例
看一下右邊的這個圖表:
它顯示了一個Logger類只有一個屬性,(也是一個實例),和兩個方法init和sharedInstance;
第一次客戶端發送sharedInstance消息,這和屬性的實例還沒有初始化,所以你要創建這個類的一個新的實例然后返回一個指針指向它。
下一次你調用sharedInstance方法,實例就會立馬返回而不需要任何的初始化,這個邏輯保證任何時候都只有一個實例存在。
你將要實現這和模式通過創建一個單例的類去管理所有的album 數據。
你將會意識到在這個組中有一個叫做API的租在這個工程中,這是你將會將所有的提供給你的app服務的類放到其中。在這個組中創建一個叫做LibraryAPI的類,繼承於NSObject。
打開LibraryAPI.h,加入以下方法:
- +(LibraryAPI*)sharedInstance;
在LibraryAPI.m中加入這些方法:
- +(LibraryAPI*) sharedInstance{
- //1
- staticLibraryAPI* _sharedInstance;
- //2
- staticdispatch_once_t once;
- //3
- dispatch_once(&once,^{
- _sharedInstance = [[LibraryAPI alloc]init];
- });
- return _sharedInstance;
- }
這個簡單的方法中有很多內容:
1. 定義了一個靜態變量去hold住類中的實例,確保它是全局可用的在你的類中。
2. 定義了一個靜態變量dispatch_once_t類型的去確保這個初始化代碼只執行一次。
3. 使用Grand Central Dispatch(GCD)去執行一個初始化LibraryAPI的實例的block,這就是單例設計模式的本質:這個初始化永遠不會再次被調用一旦這個類被實例化。
下次你調用sharedInstance方法是,這個dispatch_once里面的block代碼不會被執行了,你會得到一個先前創建這個LibraryAPI的一個引用。
現在你有了一個單例對象作為albums的入口指針,更進一步去創建一個類去處理庫中數據的持久化。
在API組中創建一個PersistencyManager的繼承於NSObject的類。打開它的.h文件,加入
#import “Album.h”再加入以下代碼:
- - (NSArray *)getAlbums;
- - (void)addAlbum:(Album *)album atIndex:(int)index;
- - (void)deleteAlbumAtIndex:(int)index;
以上是加入了三個方法去處理數據的。
打開.m文件在 @implemetation的上方加入這些代碼:
- @interface PersistencyManager () {
- // an array of all albums
- NSMutableArray *albums;
- }
上面的代碼添加了一個類擴展,這是另一個添加私有變量和方法帶類中所以外部的類並不只帶他們,在這里,你聲明了一個可變數組去hold住album的數據,這個數據是可變的以便你可以輕松地添加和刪除專輯。
現在添加下面代碼到PersistencyManager.m中去
- - (id)init
- {
- self = [super init];
- if (self) {
- albums = [NSMutableArray arrayWithArray:@[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png" year:@"1992"],
- [[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png" year:@"2003"],
- [[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png" year:@"1999"],
- [[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png" year:@"2000"],
- [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png" year:@"2000"]]];
- }
- return self;
- }
- -(NSArray *)getAlbums
- {
- return albums;
- }
- -(void)addAlbum:(Album *)album atIndex:(int)index
- {
- if (index < [albums count]) {
- [albums insertObject:album atIndex:index];
- }else{
- [albums addObject:album];
- }
- }
- - (void)deleteAlbumAtIndex:(int)index
- {
- [albums removeObjectAtIndex:index];
- }
在init這個初始化函數中你用五個實例轉會填充了這個數組,其他幾個方法允許你去得到,添加額刪除albums。編譯你的工程確保仍能正確編譯。
在這個時候你可能會疑問為什么PersistenceManager不是單例,它和LibraryAPI的關系會下下一部分的外觀(Façade)設計模式中見到。
外觀設計模式
外觀設計模式向復雜的子系統提供了簡單的接口,相比將一系列的類和他們的接口暴露給用戶,你只需要暴露一些簡單的未定義的API。
接下來的圖片解釋了這一概念。
使用這些API接口的人完全沒有意識到你這下面隱藏的復雜性,在有一系列類,特別是他們使用很復雜或者難以理解的時候,這個模式是非常好的。
外觀設計模式使用從接口層面去使用,在實現技術上隱藏而將代碼解藕了。它也減少了你外部的代碼對於內部子系統代碼的依賴性。它在外觀模式可能要進行改變的情況下也是很有用的,因為外觀的類仍然可以保持相同的API當背后的情況發生了變化時。比如,有一天你想改變背后的服務代碼,你不用去改變這些代碼因為這些API不會改變。
如何使用外觀設計模式
目前你有PersistencyManager類去本地保存album的數據,而HTTPClient去處理遠程的數據交流。工程里面的其他代碼不應該意識到這個邏輯。
要實現這個LiabraryAPI你應該hold住PersistencyManager和HTTPClient的一個實例。然后LiabraryAPI會暴露一個簡單的接口去訪問這些服務。
小貼士:通常一個單例會在app的整個生命周期都會存在,你不應該持有過多的單例指針指向其他物體,因為它們在app關閉之前不會被釋放。這個設計應該是像下面的這個圖這樣。
LiabraryAPI 會暴露給其他代碼,但是會隱藏PersistenceManager和HTTPClient針的復雜性。
打開LiabraryAPI.h,添加#import “album.h”,接下來添加這些方法的人聲明到里面。
- - (NSArray *)getAlbums;
- - (void)addAlbum:(Album *)album atIndex:(int)index;
- - (void)deleteAlbumAtIndex:(int)index;
從現在開始,這些方法將會被你暴露給外部使用。
打開LiabraryAPI.m文件。加入
- #import "HTTPClient.h"
- #import "PersistencyManager.h"
這是你導入這些類唯一的地方,記住:你的復雜的系統只能由你的API唯一的訪問。現在,添加這些私有變量通過類擴展。(在@implementation上方)。
- @interfaceLibraryAPI ()
- {
- HTTPClient *client;
- PersistencyManager *manager;
- BOOL isOnLine;
- }
isOnline決定了服務器石油應該根據album表中的變化進行更新,例如添加或者刪除albums.
你需要在init里面對它們進行初始化。在LiabraryAPI.m文件中添加下面代碼:
- -(id)init
- {
- if (self = [superinit]) {
- client = [[HTTPClientalloc]init];
- manager = [[PersistencyManageralloc] init];
- isOnLine = NO;
- }
- returnself;
- }
HTTPClient這個並不會真正的和一個服務器配合工作,在這里只是為了演示外觀設計模式,所以isOnline總是NO。接下來在LiabraryAPI.m文件中添加這些方法。
- -(NSArray *)getAlbums
- {
- return [managergetAlbums];
- }
- - (void)addAlbum:(Album *)album atIndex:(int)index
- {
- [manageraddAlbum:album atIndex:index];
- if (isOnLine) {
- [clientpostRequest:@"/api/addAlbum"body:[album description]];
- }
- }
- - (void)saveAlbums
- {
- [managersaveAlbums];
- }
- - (void)deleteAlbumAtIndex:(int)index
- {
- [managerdeleteAlbumAtIndex:index];
- if (isOnLine) {
- [clientpostRequest:@"/api/deleteAlbum"body:[@(index) description]];
- }
- }
看一下這個
- - (void)addAlbum:(Album *)album atIndex:(int)index<span style="color:black;"></span>
方法,這個類首先本地更新數據,然后如果由網絡連接,就遠程更新,這就是外觀模式的魅力,如果在你系統以外的類加入了一些新的album,它不知道,也不需要去知道,這些復雜性都被隱藏在下面了。
提醒:當為你的子系統中的類設計一個外觀模式,記住沒有什么做什么去阻止客戶端去直接訪問這些隱藏的屬性,不要假設所有的外部客戶端都需要向你在外觀模式下使用它們的方法那樣去使用它們。
編譯和運行你的程序,你會看見如下的空白的黑色屏幕。
你需要做點事情去在屏幕上去顯示album的數據-那就是接下來的下一個設計模式:裝飾設計模式。Decorator.
裝飾設計模式
裝飾設計模式動態的添加一些行為和任務到一個對象中且不需要去修改它的代碼。當然你也可以選擇用繼承的方式-通過包裝成另一個對象去改變它的行為。
在objective-c中由兩個非常常用的實現方式:分類和代理。(Category, Delegate)
Category
分類是一種非常有用的機制,它允許你去添加一些方法到已經存在的類中且不用去繼承它。這些新方法會在編譯的時候添加上去,且可以像這個被擴展的類中的其他方法一樣被執行。它和典型的裝飾設計模式由一點輕微的不同,因為它並不hold住它所擴展的類的實例。
小貼士:除了向你自己的類去添加方法,你還可以向cocoa中的類去添加方法。
如何使用分類
想像這是你想要在tableView中顯示album數據的一種方案。
這些album titles從哪里來?Album是一個模型對象,所以它並不在意你是怎么樣顯示數據的,你將要需要一些額外的代碼去將這些功能添加到Album類中去,但是不要直接去修改這些類。
你將要創建一個分類去擴展Album。它要定義一個新的方法去返回一個數據結構來被UITableViews輕松的使用。這些數據結構看起來應該是這樣的。
為了添加一個分類到Album中,新建一個文件選擇Objective-C category 模版,在category field中鍵入TableRepresentation,在Category On上寫入Album。
在Album+TableRepresentation.h加入以下方法的聲明。
- - (NSDictionary *)tr_tableRepresentation;
注意到這里有一個tr_在方法名的前面,就像一個分類的名字的前綴。這樣的命名約定有利於防止和其他方法沖突。
注意:如果在分類中聲明的方法名字和在原來的類中的方法名字一樣,或者和另一個類擴展中的方法名字一樣,那么就會顯示未定義,因為這些方法的實現是在運行時,如果你用分類去擴展你自己定義的類,那么出現問題的概率不大,但是如果你用分類添加方法到Cocoa或者CocoaTouch的類中,那有可能會產生很嚴重的問題。
在Album+TableRepresentation.m添加下面的方法
- - (NSDictionary *)tr_tableRepresentation
- {
- return @{@"title": @[@"Artist",@"Album",@"Genre",@"Year"],
- @"values": @[self.artist, self.title, self.genre, self.year]};
- }
想一下這個設計模式有多強大:
1你可以直接使用Album中的屬性。
2 你可以不繼承就可以向一個類添加方法。當然如果你想繼承的話,也可以繼承。
3 它可以簡單的讓你返回一個UITableView-ish的Album的顯示類型,而不用修改Album的代碼。
蘋果公司使用分類在很多在基礎的類上。要想去看他們是怎樣實現的,可以打開NSString.h,找到@interface NSString ,那你將會看到這個類的定義和一下三個分類緊緊聯系在一起:
NSStirngExtensionMethods, NSExtendedStringPropertyListParsing,NSStringDeprecated。分類把這些方法有序的組織起來而又分成幾部分。
代理(delegateon)
另一個裝飾設計模式就是代理,就是一個對象可以代表或者協助另一個對象的一種機制。例如,當你使用UITableView,其中你必須實現的方法就是tableView:number numberOfRowsInSection.
你不能指望UITableView去知道你想在每一個分區里面有多少行。因此,計算每個分區有多少行的任務就交給了UITableView的代理。這讓UITableView可以和它要顯示的數據進行獨立
UITableView 對象的工作是顯示一個table view,然后最終它還是需要一些它根本沒有擁有的數據。那么,它會向它的代理去發送一條消息去請求一些額外的信息。在Objective-C中的代理設計模式的實現中,一個類可以通過協議protocol聲明必須的required或者可選的optional的方法。你將會實現這些協議在這個教程的后半部分。
似乎看起來通過繼承一個對象然后去重寫它的需要的方法更容易,凡事考慮到你你只能繼承一個類。如果你向一個對象成為兩個或者更多對象的代理,那你不能通過繼承去實現這個目標。
小貼士:這是一個很重要的模式,蘋果公司使用這個方法在大部分的UIKit的類中:UITableView,UITextView,UITextField,UIWebView,UIAlert,UIAction,UICollectionView,UIPickerView,UIGestureRecognizer,UIScrollerView等等。
如何使用代理模式:
在ViewController.m中加入導入這些文件的頭文件。
- #import "LibraryAPI.h"
- #import "Album+TableRepresentation.h"
利用類擴展去添加這些私有變量。
- @interfaceViewController ()
- {
- UITableView *dataTable;
- NSArray *allAlbums;
- NSDictionary*albumData;
- intcurrentAlbumIndex;
- }
- @end
然后在@interfaceViewController ()加上 <UITableViewDataSource,UITableViewDelegate>
這是你怎樣使你的代理遵從一個協議-想像它是一個被代理去完成方法的協議的一個約定,在這里你讓ViewController去遵守UITableViewDataSource和UITableViewDelegate協議,這個方法使得UITableView可以絕對保證這些必須方法會被它的代理所實現。
接下來,在viewDidLoad中加入這些代碼:
- self.view.backgroundColor = [UIColorcolorWithRed:0.76f green:0.81f blue:0.87f alpha:1];
- currentAlbumIndex = 0;
- allAlbums = [[LibraryAPIsharedInstance]getAlbums];
- dataTable = [[UITableViewalloc]initWithFrame:CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:UITableViewStyleGrouped];
- dataTable.delegate = self;
- dataTable.dataSource = self;
- dataTable.backgroundColor = nil;
- [self.viewaddSubview:dataTable];
這里是這些代碼的講解:
1首先將背景顏色改成了一個相對友好的背景色。
2通過API而不是PersistencyManager得到了albums的列表。
3在這里創建了UITableView,你聲明了這個控制器是UITableView的delegate/dataSource,因此UITableView的所有必須的方法都會由控制器提供。
然后添加這個方法在控制器實現代碼中:
- - (void)showDataForAlbumAtIndex:(int)index
- {
- if (index < [allAlbumscount]) {
- Album *album = [allAlbumsobjectAtIndex:index];
- albumData = [album tr_tableRepresentation];
- }else{
- albumData = nil;
- }
- [dataTablereloadData];
- }
showDataForAlbumAtIndex:從albums這個數組中獲取了所需答album的數據。然后你之需要去調用reloadData.這會讓UITableView詢問它的代理諸如在table view的每個分區中顯示多少行,多少個分區,每一行應該怎么樣之類的事情。
在viewDidLoad的結尾處加上[self showDataForAlbumAtIndex:currentAlbumIndex];
這會在應用啟動的時候load現在的album,然后因為currentAlbumIndex原先被初始化為0,所以只是在會顯示第一個album。
編譯運行你的工程,你會遇到一個崩潰伴隨一個異常的顯示在調試控制台。
這怎么了?因為你聲明了控制器成為UITableView的delegate和dataSource,但是這樣的話你必須遵守去實現它的必須的方法包括numberOfRowsInSection這個你還沒實現的方法。向ViewController.m中加入這兩個方法。
- -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- {
- return [albumData[@"title"] count];
- }
- -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"Album"];
- if (!cell) {
- cell = [[UITableViewCellalloc]initWithStyle:UITableViewCellStyleValue1reuseIdentifier:@"Album"];
- }
- cell.textLabel.text = [albumData [@"title"] objectAtIndex:indexPath.row];
- cell.detailTextLabel.text = [albumData[@"values"]objectAtIndex:indexPath.row];
- return cell;
- }
前一個方法返回在table view中要顯示的行數,在這里和數據結構中的titles的數量相同。后者創建並返回了一個帶有title和value的cell。
編譯和運行工程,你的應用應該是向這樣展示在你的面前。
到目前為止,事情好像看起老很棒,但是你如果你調用第一張圖片去展示啟動后的app,那將會由一個水平的滾動條在屏幕的albums轉換之間的頂部。與其創建一個單一用途的水平滾動條,為什么不使它成為一個通用的視圖。
為了是這個視圖可重用,所有的關於它的內容的決定都該留給另一個對象-它的代理。這個horizontal scroller應該定義一些方法讓它的代理去實現以至於去和scroller一起工作,和UITableViewde的代理方法想類似。我們將會在下一個設計模式中去討論這個設計模式。
適配器設計模式(The Adapter Pattern)
適配器模式讓不同的類之間的不兼容的接口可以一起工作。它將自己包裝成一個對象,然后暴露一個標准的接口去讓外界和這個對象去交互。
如果你對適配器模式熟悉,那么你會注意到蘋果用一個稍微不同的方法去實現它-蘋果使用協議去做這個工作,你也許會熟悉像UITableViewDelegate,UIScrollViewDelegate,NSCoding,NSCopying這樣的協議,例如,通過NSCopying協議,任何的類都可以提供一個標准的copy方法。
如何使用適配器模式
這個之間提到的horizontal scroller應該是像下面的這個圖這樣子。
首先新建一個Objective-C的類,讓它繼承於UIView,打開它的.h文件,在@end的下面寫上這行代碼。
- @protocolHorizontalScrollerDelegate <NSObject>
- @end
這定義了一個名為HorizontalScrollerDelegate的協議,繼承於 NSObject協議。這是一個很好的實踐去遵從NSObject協議-或者去遵從一個已經遵從NSObject協議的協議,這會讓你可以向HorizontalScroller的代理對象發送NSObject中定義的消息。你將會看到這為什么是那么的重要。
你要定義它的代理必須和可選實現的方法在@protocol和@end之間
- @required
- - (int)numberOfviewsForHorizontalScroller:(HorizontalScroller *)horizontalScroller;
- - (UIView *)horizontalScroller:(HorizontalScroller *)scrollerviewAtIndex:(int)index;
- - (void)horizontalScroller:(HorizontalScroller *)scrollerclickViewAtIndex:(int)index;
- @optional
- - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller;
在這里你既有可選的也有必須的方法,必須的方法必須被代理實現,而通常這回包含一些這個類絕對需要的數據。在這個例子中,這些必須的分別是視圖的數量,在特定的位置的視圖和當一個視圖被點擊后的行為。這個可選的方法是初始化的使用,如果它不被代理所實現,那么默認就是第一個視圖。
接下來,你需要將這個類的定義飲用到你的代理中去。但是這個協議的定義是在累的定義之下的,因此還剩不可見的,那你該怎么辦呢。
解決的辦法奇偶說前向的定義一個協議來讓編譯器去知道這樣一個協議是可用的,所以,添加這行代碼在@interface的上方。
然后在@interface和@end之間下入如下代碼。
這個代理的屬性是weak類型的,為了防止循環引用這是必須的,如果一個類持有一個強指針指向它的代理,而它的代理也持有一個強的指針指向它,那么你的應用會因為任何一個類都不能彼此釋放內存而造成內存泄漏。而id類型表示你只可以成為遵守HorizontalScrollerDelefate的對象的assign方,給了你一定程度上的類型安全。
這個reload方法是一個在UITableView之后被重新刷新了,它reload了所有的用於構建horizontal scroller的數據。
用下面的代碼體大地HorizontalScroller.m中所有的代碼。
- #import "HorizontalScroller.h"
- #define VIEW_PADDING 10
- #define VIEW_DIMENSION 100
- #define VIEW_OFFSET 100
- @interfaceHorizontalScroller ()<UIScrollViewDelegate>
- {
- UIScrollView*scroller;
- }
- @end
- @implementationHorizontalScroller
- @end
看一下這些注釋:
1. 定義了一些常量去使更容易在設計時修改布局。視圖在這個scroller中的面積是100*100有一個和它相近的矩形有一個10。
2. HorizontalScroller遵守<UIScrollViewDelegate>,這是因為HorizontalScroller適應一個UIScrollView去滾動album,它需要去知道用戶的行為比如用戶停止了滾動。
3. 創建了一個scroll view容器。
下一步你需要去實現這個初始化方法,添加下面這個方法。
- - (id)initWithFrame:(CGRect)frame
- {
- self = [superinitWithFrame:frame];
- if (self) {
- scroller = [[UIScrollViewalloc]initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
- scroller.delegate = self;
- [selfaddSubview:scroller];
- UITapGestureRecognizer *tap = [[UITapGestureRecognizeralloc]initWithTarget:selfaction:@selector(tapAction:)];
- [scrolleraddGestureRecognizer:tap];
- }
- returnself;
- }
這個scroll view完全填充了這個HorizontalScroller,一個UITapGestureRecognizer檢測有沒有在這個scroll view上有觸摸行為和檢查一個album cover是否被點擊。如果有的話,它會通知HorizontalScroller的代理。加入這個方法。
- - (void)tapAction:(UITapGestureRecognizer *)gestureRecognizer;
- {
- CGPoint location =[gestureRecognizer locationInView:gestureRecognizer.view];
- for (int index = 0; index< [self.delegatenumberOfviewsForHorizontalScroller:self]; index++) {
- UIView *view = scroller.subviews[index];
- if (CGRectContainsPoint(view.frame, location)) {
- [self.delegatehorizontalScroller:selfclickViewAtIndex:index];
- [scrollersetContentOffset:CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0)];
- break;
- }
- }
- }
手勢被當成locationInView的一個參數去傳遞讓你精確的知道位置,接下來你喚醒了它的代理的numberOfviewsForHorizontalScroller方法,這個HorizontalScroller的實例除了知道它可以安全的向一個遵守了HorizontalScrollerDelegate的對象發送方法以外其他的一無所知。對於在uiscroll view中的每一個視圖,用CGRectContainsPoint方法去找出被點擊的視圖。當這個視圖找到了以后,向它的代理發送clickViewAtIndex消息。在不跳出這個循環之前,將這個被點擊的視圖居中。添加下面這個方法去reload這個scroller。
- - (void)reload
- {
- if (self.delegate == nil) {
- return;
- }
- [scroller.subviewsenumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOLBOOL *stop) {
- [obj removeFromSuperview];
- }];
- // 3 - xValue is the starting point of theviews inside the scroller
- CGFloat xValue = VIEW_OFFSET;
- for (int i=0; i<[self.delegatenumberOfviewsForHorizontalScroller:self]; i++)
- {
- // 4 - add a view at theright position
- xValue += VIEW_PADDING;
- UIView *view = [self.delegatehorizontalScroller:selfviewAtIndex:i];
- view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSION, VIEW_DIMENSION);
- [scrolleraddSubview:view];
- xValue += VIEW_DIMENSION+VIEW_PADDING;
- }
- // 5
- [scrollersetContentSize:CGSizeMake(xValue+VIEW_OFFSET, self.frame.size.height)];
- if ([self.delegaterespondsToSelector:@selector(initialViewIndexForHorizontalScroller:)]){
- int initialViewIndex = [self.delegateinitialViewIndexForHorizontalScroller:self];
- [scrollersetContentOffset:CGPointMake(initialViewIndex*(VIEW_DIMENSION+(2*VIEW_PADDING)), 0) animated:YES];
- }
- }
通過逐行注釋來看看這些代碼。
1. 如果沒用代理,那么沒用什么可以做的事情,那你可以直接返回了。
2. 將原先添加到scroll view中的字視圖全部移除掉。
3. 所有的視圖都在一個給丁的距離開始,現在它是100,但是可以輕松的通過改變上面那個定義的常量去改變,
4. HorizontalScroller每次詢問它的代理讓它們彼此水平的挨着在一塊。
5. 一旦所有的視圖都安置好了,設置這個滾動視圖的contenOffset讓用戶可以滾動所有的album covers。
6. 這個HorizontalScroller檢查它的代理是否響應initialViewIndexForHorizontalScroller這個方法,如果響應的話這個代碼就會將這個滾動視圖放在它的代理定義的初始視圖的中心,否則默認的就是0
當你的數據發生了改變以后你要執行reload操作,你也可以執行調用這個方法在你將HorizontalScroller添加到其他的視圖上的時候。加入一下代碼到HorizontalScroller.m文件中,
- - (void)didMoveToSuperview
- {
- [selfreload];
- }
didMoveToSuperview這個消息當一個視圖要添加到另一個視圖上作為一個子視圖上時被調用。這個時候就是reload scroller的內容的正確的時機了。HorizontalScroller的最后一個難題時確保你正在看見的album總是在scroll view的正中間。因此你們要去實現一些計算當用戶用手指拖動這個scroll view時。
將這個代碼添加到HorizontalScroller.m中
- - (void)centerCurrentView
- {
- int xFinal = scroller.contentOffset.x + (VIEW_OFFSET/2) + VIEW_PADDING;
- int viewIndex =xFinal / (VIEW_DIMENSION+(2*VIEW_PADDING));
- xFinal =viewIndex * (VIEW_DIMENSION+(2*VIEW_PADDING));
- [scrollersetContentOffset:CGPointMake(xFinal,0) animated:YES];
- [self.delegatehorizontalScroller:selfclickViewAtIndex:viewIndex];
- }
上面這段代碼計算當前的scroll view的騙一直和面積和視圖的padding去計算當前的視圖離中心的距離。最后一行是很重要的,一旦這個視圖移動到中心了,通知它的代理這個顯示的視圖被改變了。
為了檢測用戶在scroll view的拖動,你必須添加如下的UIScrollViewDelegate方法:
- - (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView
- {
- [selfcenterCurrentView];
- }
- - (void)scrollViewDidEndDragging:(UIScrollView *)scrollViewwillDecelerate:(BOOL)decelerate
- {
- if (!decelerate) {
- [selfcenterCurrentView];
- }
- }
scrollViewDidEndDragging: willDecelerate:會通知它的代理當用戶結束拖動的時候。如果用戶還沒有結束拖動那么這個decelerate參數就為真。當用戶結束拖動,那么系統就會調用前面那個方法。兩個方法中我們都調用了新的方法去讓當前的視圖居中因為這個當前的視圖已經在用戶拖動后發生了改變。
現在這個HorizontalScroller已經准備好使用了。回顧一下你剛才所寫的代碼,一點都沒有提及到album和albumView這些類,這是非常好的,因為這意味着你的代碼是獨立的和可重用的。
現在編譯使工程確保沒事。
現在HorizontalScroller已經准備好了,是時候去使用它了。打開viewController.m加入如下的代碼:
- #import "HorizontalScroller.h"
- #import "AlbumView.h"
添加HorizontalScrollerDelegate,使
- @interfaceViewController () <UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>
添加下面的這個實例變量到它的類擴展中。 HorizontalScroller *scroller;
現在你可以實現這些代理方法了,你將會驚訝只用幾行代碼就可以實現很多的功能。
添加下面的代碼到ViewController.m中
- - (void)horizontalScroller:(HorizontalScroller *)scrollerclickViewAtIndex:(int)index
- {
- currentAlbumIndex = index;
- [selfshowDataForAlbumAtIndex:currentAlbumIndex];
- }
這里設置了這些變量去存儲暫時的album然后調用showDataForAlbumAtIndex方法去顯示一個下那的album的數據。
小貼士:將一些方法放在一起通過@pragma mark是一種很好的習慣。編譯器會忽略這一行但是你會在你的Xcode的jump bar中將這些方法列起來。這會幫助你在Xcode中更好的組織代碼。然后添加下面的代碼
- -(int)numberOfviewsForHorizontalScroller:(HorizontalScroller *)horizontalScroller
- {
- return [allAlbumscount];
- }
這就是像你能辨認出來的那樣,這是協議方法返回在scroll view中的視圖的個數。因為折合scorll view為所有的album 的數據顯示covers ,這個count就是album記錄的數據。接下來添加這個方法:
- -(UIView *)horizontalScroller:(HorizontalScroller *)scrollerviewAtIndex:(int)index
- {
- Album *album = [allAlbumsobjectAtIndex:index];
- return [[AlbumViewalloc]initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl];
- }
在這里你創建了一個新的AlbumView然后將它傳遞給了horizontalScroller.
就是這么多,你只用了三個很簡短的方法就顯示了一個很好看的horizontalScroller。
是的,你仍要去真正的創建一個scroller然后將它添加到你的主要的view中但是在坐這個之前。添加這個方法:
- - (void)reloadScroller
- {
- // allAlbums = [[LibraryAPI sharedInstance]getAlbums];
- if (currentAlbumIndex < 0) {
- currentAlbumIndex = 0;
- }elseif(currentAlbumIndex >= [allAlbumscount]){
- currentAlbumIndex = [allAlbumscount]-1;
- }
- [scrollerreload];
- [selfshowDataForAlbumAtIndex:currentAlbumIndex];
- }
這個方法loads album的數據通過LibraryAPI然后設置當前啊的現實的視圖在機遇當前的視圖的index的值上,如果當前的view index 小於0,那么意味着沒有當前的視圖選擇,那就將第一張album顯示,否則就是最后一張album被顯示。
現在初始化這個scroller通過添加下面的代碼到你的viewController.m中
- scroller = [[HorizontalScrolleralloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)];
- scroller.delegate = self;
- [self.viewaddSubview:scroller];
- [selfreloadScroller];
上面的僅僅是創建了一個新的HorizontalScroller實例,添加到main view中,然后loads所有的子視圖去顯示album的數據。
小貼士:如果一個協議變得很大,然后有很多的方法,那么你應該考慮將它分解成幾個小的協議,UITableViewDelegate和UITableViewDataSource就是一個非常好的例子。嘗試着去設計你的戲而已讓每一個控制一個具體的功能。
編譯和創建米的工程,看一下你的這個很棒的新的horizontal scroller:如下圖:
等下,這個horizontal scroller是在里面了,但是這個album cover在哪里呢?
額,那九堆了-你還沒有去實現下載cover的代碼。所以你將要去添加一個下載圖片的方法,計算你的所以訪問的服務都是通過LibraryAPI,那么那里就是你放這些方法的地方,然后,首先還要考慮以下幾個問題:
1 AlbumView不應該直接和LibraryAPI去搭配工作,你不要去將視圖邏輯和交流邏輯混合起來.
2 相同的原因,LibraryAPI也不該知道有關AlbumView的事情。
3 LibraryAPI應該去通知AlbumView一旦這些covers已經下載好了因為AlbumView要去顯示它們。
聽起來像個謎,不要回信,你將會學到怎么樣去使用Observer模式.
觀察者模式
在觀察者模式中,一個對象將會通知其他對象的任何狀態的改變。這些相關的對象並不需要去知道另一個對象-這樣就造成了一個非耦合的設計。這個模式大部分用在去通知一個感興趣的對象它的一個屬性已經發生了改變。
一般的實現需要一個對象注冊成為它感興趣的狀態的觀察者,當這個狀態改變了,所有的觀察者對象都會接收到通知。蘋果的Push Notification服務就是對這個最好的例子。
如果你想要堅持MVC設計模式的概念,你需要去允許Model對象去和View對象交流,但是它們之間並沒有直接的引用,這就是觀察站模式引入的原因。
Cocoa實現觀察者模式有兩種常用的方法:Notification和Key-Value-Observing(KVO)
Notificaions
不要和和push或者本地的通知相混淆,Notifications是基於一個訂閱-分發的模型去允許一個對象發送一些消息給其他對象。這個對象不需要去知道關於訂閱者的任何信息。Notifications被蘋果公司用的很多。例如,當鍵盤顯示或者隱藏時,系統將會發送一個UIKeyboardWillShowNotification/UIKeyboardWillHideNotification,響應的當你進入到后台,系統將會發送一個UIApplicationDidEnterBackgroundNotification的通知.
提醒:打開UIApplication.h在文件的結尾你會看到一系列超過20條系統發送的通知。
如何使用Notifications
打開AlbumView.m加入下面的代碼到[self addSubView:indicator]后面:
- [[NSNotificationCenterdefaultCenter] postNotificationName:@"DownloadImageNotification"
- object:self
- userInfo:@{@"imageView":coverImage, @"coverUrl":albumCover}];
這一行通過NSNotificationCenter單例發送一個通知,這個通知的info里面包括了UIImageView去計算和要下載的cover image的URL,這就是全部的你需要去實現下載任務的信息。
添加下面的代碼到LibraryAPI的init方法中,直接在isOnline = No的后面。
- [[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(downloadImageAction:) name:@"DownloadImageNotification"object:nil];
這就是方程式的另一端,觀察者。每次一個AlbumView類發送一個DownloadImageNotification的通知的是很好,因為Library已經注冊成為它的一個觀察者,所以系統會通知它,那么它就會相應的去執行downloadImage。
然而,在你實現downloadImage之前,你必須記住當你的類deallocated的時候去取消觀察者的狀態。否則的話,一個通知可能會被發送給一個已經被deallocated的對象,那么就會導致app的崩潰。
添加下面的方法到Library中:
- - (void)dealloc
- {
- [[NSNotificationCenterdefaultCenter]removeObserver:self];
- }
當這個類結束后,它會移除它所有的通知中的觀察者狀態。
還有一件事要去做。如果你將下載的covers保存起就是一個好主意,因為我們不用一次又一次的去下載相同的covers。打開PersistencyManager.h然后添加下面的這兩個方法的原型:
- - (UIImage *)getImageFromFileName:(NSString *)fileName;
- -(void)saveAlbums;
然后將它們的實現代碼添加到.m文件中:
- -(void)saveImage:(UIImage *)image fileName:(NSString *)filename
- {
- filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@",filename];
- NSData *data = UIImagePNGRepresentation(image);
- [data writeToFile:filename atomically:YES];
- }
- -(UIImage *)getImageFromFileName:(NSString *)fileName
- {
- fileName = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@",fileName];
- NSData *data = [NSDatadataWithContentsOfFile:fileName];
- return [UIImageimageWithData:data];
- }
這幾個代碼是十分簡潔的,它下載的圖片將會被保存在Documents目錄下,然后getImage:將會是nil如果在Documents目錄下一個匹配的文件都沒有。
然后添加下面的方法到Library.m中:
- - (void)downloadImageAction:(NSNotification *)notification
- {
- UIImageView *imageView =notification.userInfo[@"imageView"];
- NSString *coverUrl =notification.userInfo[@"coverUrl"];
- imageView.image = [managergetImageFromFileName:[coverUrl lastPathComponent]];
- if ( imageView.image == nil) {
- NSLog(@"notificatiom%@",notification.userInfo);
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- UIImage *image = [clientdownloadImage:coverUrl];
- dispatch_async(dispatch_get_main_queue(), ^{
- imageView.image = image;
- [managersaveImage:image fileName:[coverUrl lastPathComponent]];
- });
- });
- }
- }
這是上面代碼的講解:
1. downloadImage會通過通知被執行,所以這個方法以通知為參數,UIImageView和image URL從通知中得到。
2. 如果先前已經下載的話那么就從PersistencyManager中去獲取圖片
3. 如果沒下載的話就通過HTTPClient去下載。
4. 當下載結束,在uiimage view 上顯示圖片,再用manager去將它在本地保存。
你又一次使用外觀設計模式去隱藏了下載圖片的復雜性,這個nofitifation根本不會去關心這個圖片是通過網頁下載的還是本地獲得的。
編譯和運行你的app,你將會看到下面這個美麗的covers在你的HorizentalScroller中。
停止你的app然后再次運行,只一道沒有任何的延時在加載covers的時候,你可以斷開網絡然后你的應用還是運行的完美無瑕。然而你會發現,這個spinning並沒有停止運行。這是怎么回事?
你在開始下載的時候開始啟動這個spingning,但是你並沒有實現在圖片開始下載完好的時候去停止它的邏輯。你可以在每次圖片已經下載完的時候發送一個notification,但是另外你可以使用另一個觀察者模式-KVO。
Key-Value_Observing(KVO)
在KVO中,一個對象可以請求去在一個具體的屬性開始變化的時候得到它的一個通知,不論這個屬性屬於它自己還是另一個對象。在這個例子里面,你可以使用KVO去觀察加載image的UIImageView中的這個image屬性的改變。
大家AlbumView.m,添加下面這個代碼到initWIthFrame:albumCover:中,添加在[selfaddSubview:indicator]之后。
- [coverImageaddObserver:selfforKeyPath:@"image"options:0 context:nil];
這添加了self,也當前的類成為coverImage的image屬性的一個觀察者。
你也需要吧去在你結束的時候取消成為觀察者.仍在AlbumView.m中添加下面的代碼:
- -(void)dealloc
- {
- [coverImageremoveObserver:selfforKeyPath:@"image"];
- }
最后,添加這個方法:
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context
- {
- if ([keyPath isEqualToString:@"image"]) {
- [indicatorstopAnimating];
- }
- }
你必須在每個你作為一個觀察者的類里面吧去實現這個方法。每次當被觀察的屬性變化時系統就會執行這個方法,在上面的方法中當image屬性改變的時候就會調用這個方法。就這樣,當一個圖片下載完成,這個spinning就會停止.
編譯運行你的工程,這個spinnning 就會消失。
小貼士:一定要記得去remove你的observers在它們deallocated時,否則當這個對象視圖像一個不存在的觀察者發送一個消息時那么你的app就會崩潰掉。
如果你運行以下app然后滾動一下這個covers,之后停止運行它,你會注意到app的狀態並沒有保存下來。你看到的最后一個視圖並沒有在應用再次啟動的時候成為默認的設置。
要改正這一點,你可以充分的利用下一個設計模式:Memento設計模式(備忘錄模式)
備忘錄模式
備忘錄設計模式將一個對象的內部狀態進行捕捉並外部化,換句話說就是你將你的東西保存在某個地方。以后這個外部話的轉台不需要借助封裝就可以被回復,也就是私有的數據還是私有的。
如何使用備忘錄設計模式
接下來將下面兩個方法添加在ViewController.m中
- - (void)saveCurrentState
- {
- [[NSUserDefaultsstandardUserDefaults] setInteger:currentAlbumIndexforKey:@"currentAlbumIndex"];
- }
- - (void)loadPreviousState
- {
- currentAlbumIndex = [[NSUserDefaultsstandardUserDefaults]integerForKey:@"currentAlbumIndex"];
- [selfshowDataForAlbumAtIndex:currentAlbumIndex];
- }
saveCurrentState將當前的album的index保存到NSUserDefaults,NSUserDefaults是一個iOS為了保存應用的具體的設置和數據而提供的一個標准的數據存儲的類。
loadPreviousState加載了先前保存的index這並不是完整的備忘錄設計模式,但是你現在達到了目的。
現在添加下面這行代碼到ViewController的viewDidLoad中的在scroller被初始化的代碼之前。
[self loadPreviousState];
這會在app啟動的時候加載先前被保存的狀態。但是你在那里保存從后台回來的的app的狀態。你要使用通知去完成這個任務。iOS在app被放到后台的時候會發送一個UIApplicationDidEnterBackgroundNotification的通知,你可以使用這個通知去調用saveCurrentState,是不是很方便啊?
加入下面的這行代碼到ViewDidLoad的結尾。
[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(saveCurrentState)name:UIApplicationDidEnterBackgroundNotificationobject:nil];
當app將要被掛起到后台的時候,ViewController就會自動地調用saveCurrentState去保存狀態。
現在添加下面的代碼:
- -(void)dealloc
- {
- [[NSNotificationCenterdefaultCenter]removeObserver:self];
- }
這會保證當ViewController被deallocate時候將這個累中從觀察者去移去。
編譯運行你的應用,拖動到某一張album,使用Command+shift+H去將app設置到后台,然后關閉你的app,重新啟動,看看原先你拖動到的那張圖片是不是設置在中間。
看起來這個album的數據是對的嘛,但是這個scroller並沒有將正確的album設置在中心,這是怎么回事?
這是因為這個可選的方法: initialViewIndexForHorizontalScroller的意義所在,因為你沒有實現這個方法,所以它總是被設置在默認的第一張。
去修正它,添加下面的代碼到ViewController中:
- - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller
- {
- returncurrentAlbumIndex;
- }
現在這個HorizontalScroller的第一張數一被設置成表示album位置的currentAlbumIndex,這是一個很好的方法去確保這個app保持個人化和可重用的雙重便利。
再次運行你的app,滾動一個 app
,停止app,再啟動,確保問題已經被解決。
如果你看看你的PersistencyManager的init方法,你會注意到這個album的數據被寫死了,而每次當PersistencyManager被創建的時候就被重新創建,但是是不是只創建一次albums的列表然后將它們保存在一個文件中更好呢?但是你怎么將你的Album數據保存再一個文件中呢?
一個選擇就是使用Album的property屬性,將它們保存再一個plist文件中然后當需要的時候重新去創建Album實例,這不是最好的選擇,因為它需要你去寫一個具體的依賴於再每一個類中的數據或者properties的代碼,例如說如果你后來創建了一個具有不同的屬性的Movie的類,那保存和重新讀取數據將要一個新的代碼去完成。
此外,你不能將一個私有變量保存在每一個類的實例中因為它們是不能被外界所訪問的。這就是蘋果公司創建這個Achiving機制的原因。
Achiving
Achiving是蘋果公司具體實現備忘錄模式之一。它將一個對象轉換成一個可以被保存后面可以回復但是不需要將私有變量暴露給外部類。
如何使用Achiving
首先你需要去通過遵守NSCoding這個協議表明這個Album類可以被壓縮打開Album.h然后將它變成遵守這個協議:
@interface Album :NSObject <NSCoding>
然后添加下面的代碼到Album.m中:
- - (void)encodeWithCoder:(NSCoder *)aCoder
- {
- [aCoder encodeObject:_titleforKey:@"title"];
- [aCoder encodeObject:_artistforKey:@"artist"];
- [aCoder encodeObject:_coverUrlforKey:@"coverUrl"];
- [aCoder encodeObject:_yearforKey:@"year"];
- [aCoder encodeObject:_genreforKey:@"genre"];
- }
- - (id)initWithCoder:(NSCoder *)aDecoder
- {
- if (self = [superinit]) {
- _title = [aDecoder decodeObjectForKey:@"title"];
- _genre = [aDecoder decodeObjectForKey:@"genre"];
- _year = [aDecoder decodeObjectForKey:@"year"];
- _artist = [aDecoder decodeObjectForKey:@"artist"];
- _coverUrl = [aDecoder decodeObjectForKey:@"coverUrl"];
- }
- returnself;
- }
你調用encodeWithCoder當你壓縮一個類的實例變量的時候,相反你調用initWithCoder當你解壓一個實例去創建一個Album的實例的時候,很簡單卻很強大。
現在這個Album類可以被壓縮那就添加時機保存和重載albums的列表的類。
添加下面的方法聲明到你的PersistencyManager.h中:
-(void)saveAlbums;
這將會在調用保存albums數據的時候被調用,下面添加這個實現到PersistencyManager.m中
- -(void)saveAlbums
- {
- NSString *path = [NSHomeDirectory() stringByAppendingString:@"/Documents/,albums.bin"];
- NSData *data = [NSKeyedArchiverarchivedDataWithRootObject:albums];
- [data writeToFile:path atomically:YES];
- }
NSKeyedArchiver黃這個album數組壓縮到一個albums.bin的文件中。當你u壓縮一個包含其他對象的對象時,那么這個Archiver會自動的嘗試去遞歸地壓縮子對象和子對象的子對象等等。在這個實例中,這個archival開始於albums-這時一個Album各種實例的數組,因為NSArray和Album都支持NSCoping接口,所以這個數組被自動的壓縮了。
現在替換PersistencyManager.m文件中的init文件。
- <pre name="code" class="objc">- (id)init
- {
- self = [superinit];
- if (self) {
- NSString *path = [NSHomeDirectory() stringByAppendingString:@"/Documents/,albums.bin"];
- NSData *data = [NSDatadataWithContentsOfFile:path];
- albums = [NSKeyedUnarchiverunarchiveObjectWithData:data];
- if (!albums) {
- albums = [NSMutableArrayarrayWithArray:@[[[Albumalloc] initWithTitle:@"Best of Bowie"artist:@"David Bowie"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png"year:@"1992"],[[Albumalloc] initWithTitle:@"It's MyLife"artist:@"NoDoubt"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png"year:@"2003"],[[Albumalloc] initWithTitle:@"NothingLike The Sun"artist:@"Sting"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png"year:@"1999"],[[Albumalloc] initWithTitle:@"Staringat the Sun"artist:@"U2"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png"year:@"2000"],[[Albumalloc] initWithTitle:@"AmericanPie"artist:@"Madonna"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png"year:@"2000"]]];
- [selfsaveAlbums];
- }
- }
- returnself;
- }
在這個新的代碼中NSKeyedUnarchiver從文件中加載這個album的數據,如果存在的話,如果不存在,它就會創建一個album數據然后立刻保存它讓下次啟動app的時候使用。
你也想要去在每次app進入到后台的時候保存album的數據,這似乎不需要但是如果后面你想要添加一個去改變album數據的選擇呢?然后你想要去確保所有的變化都被保存了的。添加下面的方法聲明到LibraryAPI.h中;
-(void)saveAlbums;
既然主要的application訪問所有的服務都是通過LibraryAPI,這就是應用怎么樣讓PersistencyManager去知道它是否需要保存數據,現在添加下面的方法實現到LibraryAPI.m中。
- - (void)saveAlbums
- {
- [managersaveAlbums];
- }
這個代碼僅僅傳遞了一個去讓PersistencyManager去保持albums的一個調用。
然后添加下面這行代碼到ViewController.m的saveCurrentState 中
- [[LibraryAPIsharedInstance]saveAlbums];
上面的代碼使用LibraryAPI去當ViewCOntroller想要去保存數據的時候觸發保存album數據的方法。
現在變異你的app去確保編譯通過。
不幸的是,沒有什么簡單的方法去檢查數據的固話是否完全正確,你可以在模擬器的Mocunments文件夾下面去檢查album數據文件是創建的,但是為了企業看到溫和的改變你需要去添加一個可以去改變album數據的草種。
與其去改變數據,如果你添加一個去刪除你不想讓它存在在albums中的一個album的選擇項的話是不是更好?此外添加一個撤銷的操作以免被誤刪除呢?
這就提供了一個很好的機會去介紹最后一個設計模式:命令模式
命令模式
這個設計模式將對象封裝成了一個請求獲取操作,這個封裝請求比一個原始的請求更加的靈活,且可以在對象之間傳遞,稍后存儲,動態修改獲取放到一個隊列之中。蘋果公司是用Target-action機制和Invocation實現的,你可以在蘋果的官方文檔中去知道更多的關於Target-Action,但是Invocation使用包含一個Target對象的NSInvocation類,一個方法選擇器和一些參數。這個對象可以根據需要動態的改變和執行,在命令模式中這是一個完美的例子,它解除耦合了發送對象和接收對象,且可以持續的堅持一個或者一系列請求。
如何使用命令模式:
早莫深入動作的invocation之前,你需要去設置撤銷(undo action)動作的框架,所以你必須定義一個UIToolBar 和undo stack(撤銷棧)上所需要的可變數組。
在ViewController.m的擴展中鍵入這些代碼:
- UIToolbar *toolBar;
- tableArray *undoStack;
這創建了一個為了這些操作而添加顯示的toolbar,還要一個數組去去作為命令隊列。
添加下面的代碼到ViewDidLoad的結尾:
- toolBar = [[UIToolbaralloc]init];
- UIBarButtonItem *undoBtm= [[UIBarButtonItemalloc]initWithBarButtonSystemItem:UIBarButtonSystemItemUndotarget:selfaction:@selector(undoAction)];
- undoBtm.enabled = NO;
- UIBarButtonItem *space =[[UIBarButtonItemalloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpacetarget:nilaction:nil];
- UIBarButtonItem *delete =[[UIBarButtonItemalloc]initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
- target:selfaction:@selector(deleteAction)];
- [toolBarsetItems:@[undoBtm,space,delete]];
- [self.viewaddSubview:toolBar];
- undoStack = [[NSMutableArrayalloc]init];
上面的代碼創建了一個toolbar,它有兩個按鈕和一個可變的空格。這個撤銷按鈕被禁止了因為這個undo stack一開始是空的。
同樣,因為這個toolbar沒有根據frame來初始化,所以這個在viewDidload中frame的大小還沒有設置。所以通過下面的一些代碼在一旦視圖的frame被最終設置好了以后設置這個frame。
- -(void)viewWillLayoutSubviews
- {
- toolBar.frame = CGRectMake(0, self.view.frame.size.height-44, self.view.frame.size.width,44);
- dataTable.frame = CGRectMake(0, 130, self.view.frame.size.width, self.view.frame.size.height-200) ;
- }
你將會添加下面的三個方法到ViewController中去處理album的三個管理操作:添加,刪除和撤銷。
第一個方法是增加一個新的album:
- - (void)addAlbum:(Album *)album atIndex:(int)index
- {
- [[LibraryAPIsharedInstance] addAlbum:album atIndex:index];
- currentAlbumIndex = index;
- [selfreloadScroller];
- }
在這里你添加了一個album,設置它的當前的album的index,然后重新加載scroller.接下來刪除方法:
- - (void)deleteAction
- {
- Album *deleteAlbum = [allAlbumsobjectAtIndex:currentAlbumIndex];
- NSMethodSignature *sig = [selfmethodSignatureForSelector:@selector(addAlbum:atIndex:)];
- NSInvocation*undoAction = [NSInvocationinvocationWithMethodSignature:sig];
- undoAction.target = self;
- [undoAction setSelector:@selector(addAlbum:atIndex:)];
- [undoAction setArgument:&deleteAlbumatIndex:2];
- [undoAction setArgument:¤tAlbumIndexatIndex:3];
- [undoAction retainArguments];
- [undoStackaddObject:undoAction];
- [[LibraryAPIsharedInstance] deleteAlbumAtIndex:currentAlbumIndex];
- [selfreloadScroller];
- [toolBar.items[0] setEnabled:YES];
- }
在上面的這些代碼里面有一些新的有趣的特性,所以解析一下:
1. 獲得要刪除的album,
2. 定義一個NSMethodSignature類型去創建一個NSInvocation,它將用於去在用戶決定撤銷一個刪除時做和刪除相反的操作。這個NSInovation需要知道三個事情:The selector(發送的消息),目標:((發送給誰)和發送的消息的一些參數。在這個例子里,消息時發送給刪除的相反方,因為當你撤銷一個刪除操作,你需要將它們重新添加這個刪除的album。、
3. 當這個undoAction被創建后你將它添加到undoStack中,這個動作將會被添加到一個數組的末尾,正像一個普通的堆棧一樣。
4. 使用LibraryAPI去從數據結構中去刪除album然后重新加載scroller。
5. 因為在undoStack中有了一個動作,所以你需要去將undo按鈕使能。
注意:當你使用NSInvocation,你需要去記住下面的三個點:
1. 參數必須通過指針來傳遞。
2.參數起始於index 2,因為0和1是保留給target 和action用的。
3. 如果這些參數有可能被銷毀(dealocate),你應該使用retainArguments.
最后添加下面的代碼用於撤銷:
- - (void)undoAction
- {
- if ([undoStackcount] > 0) {
- NSInvocation*undoAction = [undoStacklastObject];
- [undoStackremoveLastObject];
- [undoActioninvoke];
- }elseif( [undoStackcount] == 0){
- [toolBar.items[0] setEnabled:NO];
- }
- }
這個撤銷操作會pops堆棧中的最后一個對象。這個對象永遠是NSInvocation類型的而且可以通過調用invoke被激活。這回激活你你先前在album被刪除時創建的命令,然后添加背刪除的album到album列表中。因為你也可以刪除在你撤銷后最后一個對象,所以你需要判斷這個棧是否時空的,如果是的話那么將undo按鈕禁止交互。編譯和運行你的app去特使這個undo 機制。刪除這個album然后點擊撤銷操作去看看效果。
還有兩種設置模式沒用將它們應用在這個app中,但是也是很重要的:抽象工廠模式和責任鏈模式。但官方文檔去擴展你的設計模式水平
在這個教程你已經看到怎么樣去將發揮ios的設計模式的威力用很簡單和耦合性地的方式去處理很復雜的任務。你已經學習了很多關於ios設計模式的概念:MVC, 但里,代理,協議,外觀,貫徹着,備忘錄,命令。
你最后的代碼是耦合性很低的,可重用的且可讀性該,如果其他開飯看你的代碼它會立刻明白這是怎么回事和每個類的作用。
關鍵不是使用設計模式去寫你的每一行代碼而是當你在解決一個困難的問題特別是在設計應用的早期的時候意識到使用什么設計模式。它會讓你的開發工作更容易,代碼更高效。
此博文來自:http://blog.csdn.net/sanjunsheng/article/details/38071787。寫的很好作為收藏。