公司的前輩推薦我一篇在RAYWENDERLICH發表的iOS Design Pattern教學,正好我維護的專案也是這位前輩用同樣的design pattern寫出來的,所以我花了一些時間研究了一下,我發現這真是不可多得的好教學文章,正好補足了之前學習的盲點,所以我徵求了作者和網站主人的同意翻譯這篇文章。(該網站規定翻譯文章以十篇為上限,所以我這次用掉了一篇的額度 XD)原文連結:http://www.raywenderlich.com/46988/ios-design-patterns
言歸正傳,以下是我翻譯的內容,如果有翻譯錯誤的地方,請各位指正:
"iOS Design Pattern"一個你可能聽過但卻不完全瞭解的字眼,儘管許多的開發者都認為design pattern非常的重要,但是相關的文章卻非常少,而且開發者有時會忽略它。
Design Pattern使指在軟體設計常見的問題中可重複利用的模式,也可以說是一種設計範本,協助你創做容易理解和重複使用的程式碼,同時也能增加程式碼的彈性,有利於往後維護與修改。
若你不熟悉所謂的design pattern,我必須告訴你一些好消息!第一,拜Cocoa原始的設計所賜,你在無意中已經用了一堆的design pattern了!第二,跟隨這份教學的腳步,你將快速上手iOS主要的幾個design pattern。
這邊文章將會分成幾個段落,每個段落講述一個pattern並且說明:
- What 這個pattern是什麼
- Why 為什麼要使用這個pattern
- How 如何使用,在什麼情況下用,在使用時常見的陷阱
在過程中,你會寫出一個專輯瀏覽App,它將能展示你收藏的專輯與專輯相關的資訊,同時你將會熟悉以下幾種常見的設計模式:
- 創造類(Creational):Singleton and Abstract Factory
- 結構類(Structural) :MVC, Decorator, Adapter, Facade and Composite
- 行為類(Behavioral):Observer, Memento, Chain of Responsibility and Command
千萬別以為這是一篇關於設計的理論,上述的設計模式都會在你將創作的專輯瀏覽app中使用,完成後你的App將會看起來像這樣:
開始動手
下載starter project,解壓縮後用Xcode打開BlueLibray.xcodeproj。裡面並沒有很多東西,只有一個原生的ViewController和一個未實做的simple Http Client類別。
Note:
你知道當你創造一個全新的Xcode project時,你的程式碼就已經充斥著design pattern了嗎?MVC, Protocol, Singleton - 全都免費奉送喔!!
編注:
下載的檔案基本上是一個SingleView Project加上一個simple Http Client 類別,有興趣的人可以打開這個類別的.m檔看看,基本上裡面只實做了一個方法,之後我們會用這個方法從網路下載圖片。
在你開始鑽研第一個design pattern之前,你需要創建兩個類別來儲存和呈現專輯的資料。
點選"File/New/File..."(或是使用快捷鍵Command+N),選擇iOS > Cocoa Touch Class > Next。設定Class name為Album繼承自NSObjct,按下Next創造類別。
打開Album.h把以下的屬性和方法加到@interface與@end之間。
@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之間。
-(id)initWithTitle:(NSString *)title artist:(NSString *)artist coverUrl:(NSString *)coverUrl year:(NSString *)year{
//1
self = [super init];
//2
if (self) {
_title = title;
_artist = artist;
_coverUrl = coverUrl;
_year = year;
_genre = @"Pop";
}
//3
return self;
}
這邊沒什麼特殊的地方,只是一個簡單的init方法將Album物件實體化。
編注:
以上是一個很基本的init方法,之後會很常見到,我先解釋這些程式碼在幹什麼:
1: 呼叫父類別的init方法,把一個基本的NSObject(我們繼承的類別)做出來指派給self
2: 當成功實體化後,把這個類別在生成時要做的所有特殊的事情(父類別沒做的事情)在這做完,在這邊我們把Album特有的屬性塞進傳入的值。
3: 所有事情都完成了,把指標送出去供以後使用。
依照之前的方式,新增一個類別AlbumView,不過這次要繼承UIView!
Note:
如果你覺得快捷鍵好用,可以參考一下:
Command+N會新增一個檔案,
Command+Option+N會新增一個group,
Command+B會build你的project,
Command+R會run你的project。
打開AlbumView.h,把以下程式碼加到@interface與@end之間。
-(instancetype)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover;
接著請打開AlbumView.m把@implementation之後的程式碼改成以下的程式碼:
@implementation AlbumView
{
UIImageView * coverImage;
UIActivityIndicatorView * indicator;
}
-(instancetype)initWithFrame:(CGRect)frame albumCover:(NSString *)albumCover{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor blackColor];
//the coverImage has a 5 pixels margin from its frame
coverImage = [[UIImageView alloc]initWithFrame:CGRectMake(5, 5, frame.size.width-10, frame.size.height-10)];
[self addSubview:coverImage];
indicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
indicator.center = self.center;
[indicator startAnimating];
[self addSubview:indicator];
}
return self;
}
@end
你應該會注意到有一個物件變數叫做coverImage,你將用它來呈現專輯的封面圖片。第二個變數是一個indicator,它會在我們download專輯封面時旋轉。
在實做建構子時,你把背景設定為黑色,創造了一個image view距離邊界5 pixle並且加入了一個activity indicator。
Note:
你會許會好奇為什麼我們把變數定義在implementation區段而不是在.h檔的interface區段,這是因為別的物件不需要知道這幾個變數的存在,它們只會用在implementation區段的方法中。當你在寫library或是framework給其他開法者使用時,這麼做是非常重要的。
Build你的project(command+B)確認到目前為止的程式碼都沒有錯誤。如果都對,準備好迎接你用到的第一個design pattern :)
MVC - Design Pattern之王
Model View Controller(MVC)無疑的是Cocoa中最常被使用的design pattern。它將你的app分成三種角色,並有助於將程式碼乾淨的切割開來。
三種角色分別是:
- Model: 這類的物件負責存放和定義如何處理你的app中用到的資料。例如:我們剛剛做的Album類別。
- View:這類的物件負責呈現Model和與使用者互動,基本上UIView和其子類別都算是View。例如:我們剛剛做的AlbumView類別。
- Controller:controller負責協調所有運作,它會從model中存取資料在view上顯現,偵聽事件,如果需要的話也會編輯資料。猜猜看你的專案中哪個類別屬於controller的角色?沒錯,就是ViewController。
理想的狀態下,在你的app中所有的物件都會屬於三種角色的其中一種。
Veiw和Model之間的溝通必須透過Controller,如下圖:
Model會通知Controller資料改變,然後Controller更新View的資料。View會通知Controller使用者觸發了什麼事件,Controller會更新Model或是做出任何相應的處置。
你或許會好奇,為什麼不能把所有的東西都寫在Controller裡面呢?這樣看起來不是單純許多嗎?
之所以這麼做,完全是為了把程式碼切開使之容易閱讀
、維護與重複使用。理想情況下,所有的View都必須完全和Model分開,如此一來我們可以重複使用同一個View去呈現不同的Model。
例如,在未來你想要為你的app增加電影與書籍的資料,你可以使用AlbumView來呈現電影與書籍的圖片。甚或,你想要寫另外一個app處理音樂專輯相關的事物,你也可以重複使用Album這個類別,畢竟他可以為任何的View提供資料。這就是MVC的強大之處!
如何使用MVC Pattern
首先,你必須先確認你project中的所有類別分屬於Model、View或Controller。專案進行到此,你創造了Album類別與AlbumView類別。
接著,在project中為這三種角色創造group來放置屬於他們的類別檔。
點選"File\New\Group(或使用快捷鍵command + Option + N)來建立新的group,並將三個group分別命名為Model、View與Controller 。
現在,將Album.h與Album.m放到Model group,把AlbumView.h和AlbumView.m放到View group,最後把ViewController.h和ViewController.m放到Controller group裡面。
完成之後,你的project看起來應該像這樣:
少了四散各處的檔案,你的專案目前看起來好多了。當然,你的專案中還是可以有其他的group和類別,不過整個app的核心都會放在這三個group中。
現在你的所有元件都被分門別類的安置好了,你需要從某處拿到專輯的資料,接著你將會創造一個API類別,用它在你的程式中管理所有的資料,這是個絕佳的機會讓我們來介紹你將學會的另一個design pattern——Singleton。
Singleton設計模式
Singleton確保該類別只有一個實體物件存在,通常會用一個lazy loading的方法去創造第一個實體物件。
Note:
Apple也用了很多Singleton,例如:[NSUserDefaults standardUserDefaults], [UIApplication shareApplication], [UIScreen mainScreen], [NSFileManager defaultManager]全都會回傳一個Singleton的物件。
你可能會問,為什麼你需要確保只有一個實體存在,記憶體很便宜不是嗎?
這是因為有些特殊的情況下這麼做是非常合理的,例如:當你需要編輯Log檔時,你不會想要多個Logger實體,除非你想一次編輯多個Log檔。較理想的方法是用一個全域的類別來控制它,這樣我們可以避開多個實體物件同時存取一個Log檔的情況。
如何使用Singleton Pattern
請參考下圖:
上圖表示一個Logger類別有一個單一屬性(那就是他那獨一無二的實體),和兩個方法:shareInstance與init。
第一次呼叫shareInstance時,instance屬性尚未被初始化,所以你會創造一個物件實體指派給instance屬性。
之後你再次呼叫shareInstance時,他會檢查instance是否存在,若存在就直接回傳instance,不再做任何實體化的動作。
你將會創造一個Singleton的類別來管理所有專輯的資料。
你可能有發現project裡面有一個group叫做API,你將會把所有為app提供服務的類別放進去。在這個group裡面創造一個繼承NSObjcct的類別,命名為LibraryAPI。
編注:
這邊翻譯的有些繞口,其實API group裡面放的類別是協助你的app和外界溝通或存取資料的類別,像是寫入檔案、存取資料庫等,將這些動作放在一些類別內執行以免這些程式碼散落在project各處。
打開LibraryAPI.h把以下的程式碼填入:
@interface LibraryAPI : NSObject
+(LibraryAPI*)shareInstance;
@end
接著,打開LibrayAPI.m在@interface後插入以下程式碼:
+(LibraryAPI *)shareInstance{
//1
static LibraryAPI * _shareInstance = nil;
//2
static dispatch_once_t oncePredicate;
//3
dispatch_once(&oncePredicate, ^{
_shareInstance = [[LibraryAPI alloc]init];
});
return _shareInstance;
}
這次我們遇到很多陌生的語法,以下將一一解釋:
- 宣告一個靜態變數來存放這個類別的實體(這就像上面圖示的+instance:Logger)
- 宣告一個靜態的dispatch_once_t類別的變數,我們之後會利用它來檢查實體化的方法有沒有被執行過。
- 利用Grand Central Dispatch(GCD)來執行一個block以實體化LibraryAPI。這是一個典型的Singleton design pattern,當實體建立後,建構子永遠不會被呼叫。
當你在次呼叫shareInstance,dispatch_one的block將不會被執行,你將會直接收到之前實體化的物件指標。
編注:
你或許注意到我們使用dispatch_once_t類別的變數時沒有給他一個初值!這是因為它算是一個旗標,當我們執行dispatch_onece的block前,會判斷dispatch_once_t的記憶體位置是否有值,如果沒有值就會執行block並起塞一個值進去dispatch_once_t的記憶體位置。下次再次執行這個方法時,dispatch_once_t的記憶體位置就有值了,block就不會被執行,直接回傳_shareInstance。這也是我們為什麼要使用靜態變數的原因。
你現在有了一個Singleton的物件作為管理專輯資料的單一窗口,接著進一步創造一個類別來管理app資料。
在API group中創造一個繼承自NSObjct的類別,命名為PersistencyManager。
打開PersistencyManager.h檔,在開頭的部份增加下面的code來import Album.h檔。
#import "Album.h"
接著,在@interface之後增加以下的程式碼:
-(NSArray*)getAlbums;
-(void)addAlbum:(Album*)album atIndex:(int)index;
-(void)deleteAlbumAtIndex:(int)index;
你宣告了三個方法來處理專輯資料
打開PersistencyManager.m在@implementation之前增加以下程式碼:
@interface PersistencyManager ()
{
//an array of all albums;
NSMutableArray * albums;
}
@end
以上的程式碼增加一個extension,這是另一種增加私有方法、變數或屬性的方式。你在這裡宣告了一個NSMutableArray來存放專輯資料(裡面放的是Album類別的物件)
現在把下面的程式碼加到PersistencyManager.m的@implemetation之後:
- (id)init
{
self = [super init];
if (self) {
// a dummy list of albums
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;
}
在init方法中,你將albums陣列創造出來並且填入了五個Album(還記得我們的Model嗎!)類別的資料。如果你不喜歡這五張專輯,你也可以換成你喜歡的專輯喔!
接著,把下面的方法加入PersistencyManager.m檔案中:
-(NSArray *)getAlbums{
return albums;
}
-(void)addAlbum:(Album *)album atIndex:(int)index{
if ([albums count]>=index) {
[albums insertObject:albums atIndex:index];
}else{
[albums addObject:album];
}
}
-(void)deleteAlbumAtIndex:(int)index{
[albums removeObjectAtIndex:index];
}
這些方法可以讓你新增、刪除與查詢專輯的資料。
Build你的project確保目前為止沒有什麼問題。
進行到這邊,你也許會好奇,PersistencyManager類別並不是一個singleton的類別,那它和LibraryAPI類別又有什麼關係呢?我們又為什麼要疊床架屋把單純的NSMutableArray包在一個類別裡面,寫額外的方法存取它呢?答案將會在下一個段落揭曉,你將會學習到Facade設計模式。
Facade Pattern
Facade設計模式避免你將複雜的子系統曝露出來,取而代之的是簡單的單一窗口。
下圖解釋這個設計模式的概念:
API的使用者(在這個範例中你就是API的使用者,但如果是合作開發,使用者可能就是別的開發者囉!)完全不會察覺到底層有複雜的子系統。需要和大量其他類別互動時,Facade設計模式可以發揮強大的作用,也可用在把一些艱澀難懂的類別包裝起來,方便其他人使用。
Facade有助於將程式碼切割開,也能減少API使用者與底層系統的相依性。日後若需要維護程式碼,能有效地控制影響範圍。舉例來說:如果日後你需要更換後台的伺服器,你不需要更改使用API的程式碼,Facade有效地把更動的範圍縮減在這個API底層。
如何使用Facade Pattern
目前你已經有PersistencyManager儲存本機的專輯資料、HTTPClient處理遠端通訊。project中其他的類別不應該察覺到這些處理邏輯。
為了實現Facade設計模式,只有LibraryAPI需要有PersistencyManager和HTTPClient的實體物件。而LibraryAPI將會把其他類別不需要知道的處理邏輯隱藏起來,只曝露出簡單的API來存取這些物件。
Note:
通常singleton物件的存續時間應該和app一樣,所以你不該保留太多的strong指標在singleton物件內,因為到這個app結束前,這些指標都不會被release。
這個設計模式看起來像下圖:
如之前所說,只有LibraryAPI會被其他程式碼使用,HTTPClient和PersistencyManager會被藏在LibraryAPI後頭。
打開LibraryAPI.h把下面的程式碼加到頂端:
#import "Album.h"
接著把下面的方法定義在LibraryAPI.h內:
-(NSArray *)getAlbums;
-(void)addAblum:(Album*)album atIndex:(int)index;
-(void)deleteAlbumAtIndex:(int)index;
這是你目前會暴露給其他類別使用的方法。
打開LibraryAPI.m增加以下程式碼:
#import "PersistencyManager.h"
#import "HTTPClient.h"
之所以把import指令放在.m檔,是因為你的API是底層複雜子系統的單一窗口。
現在增加一些私有的物件變數在.m檔extension中(加在@implementation之前):
@interface LibraryAPI ()
{
PersistencyManager * persistencyManager;
HTTPClient * httpClient;
BOOL isOnline;
}
@end
isOnline判斷伺服器是否更新資料,像是新增或是刪除專輯。
你需要在init方法中初始化這些變數,把下面的程式碼增加到LibraryAPI.m檔中:
-(id)init{
self = [super init];
if (self) {
persistencyManager = [[PersistencyManager alloc]init];
httpClient = [[HTTPClient alloc]init];
isOnline = NO;
}
return self;
}
在這個範例中httpClient不會真的和後端的伺服器溝通,它只會被用來展示Facade如何應用,所以isOnline將會被設定為NO。
接著把下面的方法加入LibraryAPI.m檔中:
-(NSArray *)getAlbums{
return [persistencyManager getAlbums];
}
-(void)addAblum:(Album *)album atIndex:(int)index{
[persistencyManager addAlbum:album atIndex:index];
if (isOnline) {
[httpClient postRequest:@"/api/addAlbum" body:[album description]];
}
}
-(void)deleteAlbumAtIndex:(int)index{
[persistencyManager deleteAlbumAtIndex:index];
if (isOnline) {
[httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]];
}
}
我們來看看addAlbum:atIndex:這個方法,LibraryAPI會先更新本機資料,如果有網路連線就會接著更新伺服器的資料。這就是Facade的強大之處!當其他的類別需要增加一個專輯時,它不會知道也不需要知道背後的複雜過程。
Note:
當使用Facade來包裝你的子系統時,請記得你無法預防使用者(其他開發者)存取你想包覆住的類別。不要吝嗇使用防禦型的設計方式,也不要假設使用者會依照Facade使用這些類別的方式使用它們。
Run你的app,你會看到一片漆黑,就像下圖一樣
你需要再花點功夫來把專輯資料顯示到螢幕上,剛好給了我們應用下一個design pattern——Decorator的機會。
Decorator Pattern
Decorator設計模式可以動態增加一個物件的行為而且不用修改類別的原始碼。除了繼承之外,你還可以用這種方式為一個類別增加額外的行為或是將兩個類別的行為合併。
在Objective-C裡面,有兩種常見的方法來達到Decorator的效果:Category與Delegation。
Category
Category是個極度強大的機制,可以讓你在不用繼承的狀態下為一個類別增加方法。這個新的方法會在compile時被加入該類別,使用起來就像一般的方法。
Note:
Category除了可以擴充你自製的類別,你也可以用來擴充Cocoa內建的類別喔!
如何使用Category
想像你要把Album類別的物件用TableView來顯示:
專輯的名稱要從哪來呢?Album是一個Model物件,所以它並不會管你怎麼呈現資料。在不修改Album類別的前提下(之前說過Model類別不能和View扯上關係!),你需要增加額外的方法來把資料輸出成你要的格式。
你將會擴充你的Album類別,增加一個新方法讓Album輸出的資料能夠輕易的用在UITableView上。
輸出的資料結構會像這樣:
點選File\New\Objective-C File...選擇Objective-c category,不要選成Objective-C Class!將category命名為TableRepresentation。
Note:
你有沒有注意到檔案名稱?Album+TableRepresentation代表你正擴張Album類別。這個慣例非常重要,因為它讓你還有其他開發者可以一眼看出這是個category。
打開Album+TableRepresentation.h檔宣告以下的方法:
-(NSDictionary*)tr_tableRepresentation;
請注意我們在方法的開頭增加tr_這是category名稱的縮寫。這個習慣可以幫助我們分辨category的方法和原本的方法(也可以避免命名衝突)。
Note:
當category內的方法名稱和原來類別內的方法名稱重複,或是和原類別(或是其父類別)的其他category的方法名稱重複,呼叫方法的行為會是undefined。當category中重複定義了Cocoa類別的方法時,這個機制能避免嚴重的問題發生。
打開Album+TableRepresentation.m檔並增加以下的方法:
-(NSDictionary *)tr_tableRepresentation{
return @{@"titles":@[@"Artist" ,@"Album" ,@"Genre" ,@"Year"],
@"values":@[self.artist,self.title,self.genre,self.year]};
}
現在來看看這個強大的pattern做了什麼:
- 你在裡面用了Album的屬性。
- 你在未使用繼承的狀態下就為Album類別增加了方法,當然如果使用繼承也能達到相同的效果。
- 你沒修改Album類別的程式碼卻能傳回一個方便UITableView讀取的資料型態。
Apple用了大量的Category在Foundation類別中。你可以打開NSString.h檔,找到@interface NSString,你可以看到裡面包含了三個category:NSStringExtensionMethods, NSExtendedStringPropertyListParsing和NSStringDeprecated。這些category讓這些方法可以被分門別類地擺到個個區段內。
Delegation
另一個Decorator design pattern——Delegation,它可以讓一個物件支援另一個物件。例如,當你使用UITableView時,你必須要實作tableView:numberOfRowInSection:。
你不能期望你的UITableView會知道你想在一個section裡面放多少個row,因此計算要放多少row的工作自然會落到UITableView dataSource上,這可以讓UITableView獨立於它將呈現的資料(又是另一個有助於你將Model與View切割的技巧!)。
下圖是當你創造一個UITableView時它和ViewController之間的互動:
UITableView物件會負責顯示一個表格清單,但它終究需要得到一些它未知的資訊(所以它會去詢問delegate,在這個案例中ViewController擁有這個delegate)。在Objective-C中實作delegate pattern時,需要delegate的類別會宣告一個protocol定義optional和required的方法,你將會在這個教學中學到如何製作自己的protocol。
Note:
這是個很重要的design pattern。Apple在大部分的UIKit類別中都有使用它,例如:UITableView, UITextView, UITextField, UIWebView, UIAlertView⋯⋯你會不斷的遇到它!
如何使用Delegate
打開ViewController.m把匯入下列的類別:
#import "LibraryAPI.h"
#import "Album+TableRepresentation.h"
現在,增加一些物件變數:
@interface ViewController ()
{
UITableView * dataTable;
NSArray * allAlbums;
NSDictionary * currentAlbumData;
int currentAlbumIndex;
}
@end
接著把@interface那一行改成以下的程式碼:
@interface ViewController ()<UITableViewDataSource, UITableViewDelegate>
這邊宣告這個類別將會實作這兩個protocol定義的方法,這樣一來UITableView就能夠在這個類別中拿到它所需要的方法。
把viewDidLoad:的程式碼改成以下的樣子:
- (void)viewDidLoad
{
[super viewDidLoad];
//1
self.view.backgroundColor = [UIColor colorWithRed:0.76f green:0.81f blue:0.87f alpha:1];
currentAlbumIndex = 0;
//2
allAlbums = [[LibraryAPI shareInstance] getAlbums];
//3
//the uitableview that present the album data
dataTable = [[UITableView alloc]initWithFrame:CGRectMake(0, 120, self.view.bounds.size.width, self.view.bounds.size.height-120) style:UITableViewStyleGrouped];
dataTable.delegate = self;
dataTable.dataSource = self;
dataTable.backgroundView = nil;
[self.view addSubview:dataTable];
}
以上的程式碼做了這些事:
- 背景顏色變為海軍藍。
- 從API取得所有專輯的資料,注意你不會在這接觸到PersistencyManager!
- 這段程式碼創造一個UITableView物件,把delegate和datasource指定給目前的view controller。
接著在VeiwController.m中新增以下方法:
-(void)showDataForAlbumAtIndex:(int)albumIndex{
//防禦機制,確保albumIndex小於allAlbums的元素個數
if (albumIndex < [allAlbums count]) {
//取得專輯資料
Album * album = allAlbums[albumIndex];
//用字典的形式儲存album的資料
currentAlbumData = [album tr_tableRepresentation];
}else{
currentAlbumData = nil;
}
//拿到了我們想要的資料後重新整理我們的tableView
[dataTable reloadData];
}
showDataForAlbumAtIndex:從專輯陣列中提取我們需要的資料。當你想要刷新tableView的資料只要呼叫reloadData方法即可,它會讓UITableView重新呼叫它的delegate和datasource重新呈現tableView裡面的資料。
將下面這行程式碼加到viewDidLoad:的最後一行
[self showDataForAlbumAtIndex:currentAlbumIndex];
這會讀取app目前顯示的專輯資料,而我們剛剛把currentAlbumIndex設定為0,所以它會呈現專輯陣列裡的第一個元素。
Run你的project,如果沒有意外,你的app會當機,並且顯示以下的錯誤訊息:
發生了什麼事?因為你剛剛稱你的ViewController是一個UITableView的dataSource和delegate,但是你卻沒有實作它們所需的方法,例如:tableView:numberOfRowsInSection:方法。
將以下的程式碼加到ViewController.m中的@implementation和@end之間。
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return [currentAlbumData[@"titles"] count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
}
cell.textLabel.text = [currentAlbumData[@"titles"] objectAtIndex:indexPath.row];
cell.detailTextLabel.text = [currentAlbumData[@"values"] objectAtIndex:indexPath.row];
return cell;
}
tableView:numberOfRowsInSection:回傳tableView裡共有多少列。這邊我們回傳currentAlbumData的title數。
tableView:cellForRowAtIndexPath:將會創造並且回傳tableView中顯示的UITableViewCell物件。
Run你的app你將會看到以下的畫面出現在你的螢幕上:
回頭看看我們的第一張圖片,在這個tableView上面應該要有一個水平方向的scroller讓使用者可以選擇不同的專輯。與其專為這個app製作一個scroller,不如做一個可以重複使用的scroller!
為了讓這個scroller可以重複使用,我們必須讓別的物件為它提供顯示的內容——delegate,就像tableView的datasource一樣,我們的scroller必須宣告一些方法讓它的delegate去實作,為我們提供需要的資料。我們將會在下一個design pattern中討論並實作這個scroller元件。
Adapter Pattern
Adapter讓不同的類別可以一起作業,它把自己和別的物件綁在一起,並且提供其他物件一個標準的互動方式。
如果你熟悉Adapter Pattern你會發現Apple應用它的手法有一點點特殊,Apple用protocol來達到目的。也許你很熟悉這些protocol:UITableViewDelegate
、
UIScrollViewDelegate
、NSCoding
和NSCopying
。舉例來說,如果任何類別遵守NSCopy protocol,它就提供一個標準的copy方法。
如何使用Adapter Pattern
之前提到的水平scroller會像下面的圖片:
在你的View group裡面新增一個繼承自UIView的類別,命名為HorizontalScroller。
打開HorizontalScroller.h檔,把下面的程式碼加到@end之後。
@protocol HorizontalScrollerDelegate <NSObject>
//在這宣告方法
@end
就像你之前宣告新的類別一樣,你剛剛宣告了一個繼承自NSObject protocol的protocol並且命名為HorizontalScrollerDelegate。讓自定義的protocol遵守NSObject protocol是一個很重要的動作,這可以讓自定義的protocol藉由NSObject protocol傳送資訊,之後我們將會讓你知道為什麼這很重要。
在@protocol和@end之間加入以下的程式碼,以定義delegate必須要實作的方法和選擇性實作的方法。
@required
//向delegate詢問它想要放多少個View在HorizontalScroller裡面
-(NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller;
//要求delegate回傳各個index要顯示的view
-(UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;
//通知delegate哪個view被使用者點選
-(void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index;
@optional
//詢問delegate在一開始時要顯示哪個view,這是選擇性實作的方法,如果沒有實作就會顯示第0個view
-(NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;
上面的程式碼宣告了delegate必須實作的方法與選擇性實作的方法,必須實作的方法通常包含這個類別所需要的資料,沒有這些資料這個類別就無法運作。對HorizontalScroller來說,知道要顯示幾個view和view的內容是什麼以及告訴delegate哪個view被按了都是必備的方法。選擇性實作的方法負責告訴HorizontalScroller一開始要顯示哪個view,如果沒有實作就顯示第一個view。
接著我們要為HorizontalScroller新增一個delegate屬性,但是我們的protocol在@interface之後宣告,HorizontalScroller這時候並不『知道』protocol的存在,為了解決這個問題,我們在@interface之前增加以下程式碼。
@protocol HorizontalScrollerDelegate;
在HorizontalScroller.h檔的@interface和@end之間加上下面的程式碼。
@property (weak) id<HorizontalScrollerDelegate>delegate;
-(void)reload;
請注意,在宣告delegate時必須要加入(weak)的描述,這是用來避免retain cycle造成memory leak。當一個類別持有一個strong的delegate指標,而它的delegate也持有這個類別的strong指標,你的app就會發生memory leak,因為這兩個類別都不會release彼此的指標,記憶體無法被回收。
id用來表示delegate只能被指派給已經宣告要實作HorizontalScrollerDelegate的類別。
reload這個方法就像UITableView的reloadData方法一般,它會將HorizontalScroller所需的資料重新載入。
(未完待續)