原文地址:https://www.raywenderlich.com/46988/ios-design-patterns
iOS設(shè)計(jì)模式——你可能通過(guò)這個(gè)術(shù)語(yǔ),但是你知道它究竟是什么嗎熬尺?雖然程序員們認(rèn)為設(shè)計(jì)模式是非常重要的粱哼,但是并沒(méi)有很多關(guān)于設(shè)計(jì)模式的文章润文,并且當(dāng)我們敲代碼的時(shí),很多時(shí)候并沒(méi)有過(guò)多的去注意它們。
在軟件開(kāi)發(fā)中設(shè)計(jì)模式被用來(lái)解決一些常見(jiàn)的問(wèn)題兵扬。
他們是模板器钟,可以幫助你寫(xiě)出那些容易理解和可重用的代碼傲霸,也可以幫助你創(chuàng)建松耦合的代碼穆役,使你不需要太多的麻煩就可以在你的代碼中修改或者替換你的組件梳杏。
如果你是設(shè)計(jì)模式新手十性,那么我有一個(gè)好消息給你劲适!首先,多虧了Cocoa自身包含了許多設(shè)計(jì)模式愕贡,所以你已經(jīng)使用了許多iOS設(shè)計(jì)模式促绵,鼓勵(lì)你去使用它們是最好的實(shí)踐败晴。其次,這個(gè)教程將會(huì)帶著你去掌握所有主要的(和不是那么主要的)iOS設(shè)計(jì)模式尖坤,這些設(shè)計(jì)模式在Cocoa中被經(jīng)常使用稳懒。
這個(gè)教程分為多個(gè)部分,一個(gè)部分一個(gè)設(shè)計(jì)模式慢味。在每個(gè)部分中场梆,你將會(huì)讀到下面的一個(gè)例子:
- 這個(gè)設(shè)計(jì)模式是什么纯路。
- 為什么我應(yīng)該使用它或油。
- 怎么使用它,以及什么地方只用它驰唬,當(dāng)使用它時(shí)要小心的常見(jiàn)陷阱顶岸。
在這個(gè)教程中你將會(huì)創(chuàng)建一個(gè)音樂(lè)專(zhuān)輯app腔彰,這個(gè)app可以展示你的唱片集合和它們的相關(guān)信息。
在開(kāi)發(fā)app的整個(gè)過(guò)程中辖佣,你將會(huì)掌握以下最最常用的Cocoa設(shè)計(jì)模式:
- Creational:?jiǎn)卫统橄蠊S霹抛。
- Structural:MVC,裝飾者卷谈,適配器杯拐,外觀,符合雏搂。
- Behavioral:觀察者藕施,備忘錄,責(zé)任者鏈凸郑,命令裳食。
不要被誤導(dǎo)認(rèn)為這是一篇純理論的文章,你將在你的音樂(lè)app中使用許多這些設(shè)計(jì)模式芙沥,學(xué)完整個(gè)教程你的app將會(huì)是下面這個(gè)樣子:
我們開(kāi)始吧诲祸!
下載starter project,解壓zip文件而昨,使用Xcode打開(kāi)項(xiàng)目文件 BlueLibrary.xcodeproj 救氯。
開(kāi)始的項(xiàng)目并沒(méi)有太多的內(nèi)容,僅僅默認(rèn)的 ViewController 和一個(gè)沒(méi)有實(shí)現(xiàn)的簡(jiǎn)單的HTTP客戶端歌憨。
注意:你知道嗎着憨?一旦你創(chuàng)建了一個(gè)新的Xcode項(xiàng)目,你的代碼中就已經(jīng)使用了設(shè)計(jì)模式务嫡。MVC甲抖,代理,協(xié)議心铃,單例——你是免費(fèi)得到他們的哦准谚!
在你沉浸在第一個(gè)設(shè)計(jì)模式之前,你必須創(chuàng)建鏈各個(gè)類(lèi)去存儲(chǔ)和展示唱片集數(shù)據(jù)去扣。
使用"File\New\File..."(或者使用快捷鍵 Command+N)柱衔。選擇iOS>Cocoa Touch 然后選擇Objective-C class 點(diǎn)擊Next。設(shè)置類(lèi)的名子是Album愉棱,父類(lèi)是NSObject唆铐,點(diǎn)擊Next,然后點(diǎn)擊Create奔滑。
打開(kāi)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;
標(biāo)記所有屬性是 readonly艾岂,是因?yàn)楫?dāng)Album對(duì)象創(chuàng)建后,沒(méi)有必要去改變他們档押。
對(duì)象的初始化方法澳盐,當(dāng)你創(chuàng)建一個(gè)新的album祈纯,你將要傳遞name,artist叼耙,cover URL和year腕窥。
打開(kāi)Album.m在@implementation和@end之間加入以下代碼:
- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year
{
self = [super init];
if (self)
{
_title = title;
_artist = artist;
_coverUrl = coverUrl;
_year = year;
_genre = @"Pop";
}
return self;
}
到現(xiàn)在并沒(méi)有神奇的地方,僅僅是一個(gè)簡(jiǎn)單的初始化方法去創(chuàng)建新的Album實(shí)例筛婉。
再來(lái)簇爆,使用 File\New\File... 選擇Cocoa Touch然后選擇 Objective-C class點(diǎn)擊Next,設(shè)置類(lèi)的名字是AlbumView爽撒,但是這次設(shè)置父類(lèi)是 UIView入蛆,點(diǎn)擊Next然后Create。
注意:如果你覺(jué)得使用快捷鍵更方便硕勿, Command+N 是新建文件哨毁, Command+Option+N 是新建一個(gè)組, Command+B 是編譯項(xiàng)目源武, Command+R 是運(yùn)行項(xiàng)目扼褪。
打開(kāi)AlbumView.h 在@interface和@end之間添加下面代碼:
- (id)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover;
現(xiàn)在打開(kāi)AlbumView.m 在@implementation后面用下面的代碼替換所有代碼。
@implementation AlbumView
{
UIImageView *coverImage;
UIActivityIndicatorView *indicator;
}
- (id)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] init];
indicator.center = self.center;
indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
[indicator startAnimating];
[self addSubview:indicator];
}
return self;
}
@end
你注意到第一件事這里有一個(gè)名為coverImage的實(shí)例變量粱栖,這個(gè)變量用于展示專(zhuān)輯的封面圖片话浇,第二個(gè)變量是一個(gè) indicator,當(dāng)圖片正在下載時(shí)闹究,它旋轉(zhuǎn)著表明程序正常運(yùn)行幔崖。
在初始化方法的實(shí)現(xiàn)中設(shè)置背景顏色為黑色,創(chuàng)建的imageview 有5個(gè)像素的邊距渣淤,創(chuàng)建并添加活動(dòng)指示器赏寇。
注意:好奇想知道為什么私有變量被定義在實(shí)現(xiàn)文件中,而不是接口文件中嗎砂代?這是因?yàn)樵贏lbumView類(lèi)的外面沒(méi)有類(lèi)需要知道這些變量的存在蹋订,他們僅在類(lèi)的內(nèi)部方法的實(shí)現(xiàn)中被使用率挣。如果你創(chuàng)建一個(gè)庫(kù)(library)或者框架(framework)給其他的程序員去使用刻伊,那么這個(gè)約定是非常特別及其重要的。
編譯你的項(xiàng)目( Command+B ),確保一切井然有序椒功。都好了嗎捶箱?
那么準(zhǔn)備好你的第一個(gè)設(shè)計(jì)模式來(lái)了!:]
MVC——設(shè)計(jì)模式之王
模型 視圖 控制器(MVC)是Cocoa的眾多設(shè)計(jì)模式中的一個(gè)动漾,并且毫無(wú)疑問(wèn)的是所有設(shè)計(jì)模式中使用最多的設(shè)計(jì)模式之一丁屎。在你的應(yīng)用中根據(jù)角色給對(duì)象分類(lèi),根據(jù)角色拆分代碼旱眯,使代碼更整潔晨川,條理清晰证九。
這三種角色分別是:
模型:控制你的應(yīng)用的數(shù)據(jù)以及定義如何操作數(shù)據(jù)。例如共虑,在你的應(yīng)用中模型是Album類(lèi)愧怜。
視圖:控制模型的視覺(jué)表現(xiàn)以及用戶的交互行為;基本上妈拌,所有的UIViews以及他們的子類(lèi)都屬于視圖拥坛。在你的應(yīng)用中AlbumView屬于視圖。
控制器:協(xié)調(diào)所有工作的中間者尘分,它從模型中獲取數(shù)據(jù)然后將數(shù)據(jù)在視圖上展示猜惋,在必要的時(shí)候監(jiān)聽(tīng)事件,并且操作數(shù)據(jù)培愁。你能夠猜到哪個(gè)類(lèi)是你的控制器嗎著摔?沒(méi)錯(cuò),就是:ViewController定续。
在你的應(yīng)用中這個(gè)設(shè)計(jì)模式的好的實(shí)現(xiàn)意味著每個(gè)對(duì)象都有屬于自己的組梨撞。
下圖很好的描述了在模型和視圖之間通過(guò)控制器進(jìn)行交流:
模型通知控制器數(shù)據(jù)的改變,相反的香罐,控制器在視圖里更新改變后的數(shù)據(jù)卧波。
視圖可以通知控制器用戶執(zhí)行的動(dòng)作,控制器根據(jù)需要更新模型或者重新獲取請(qǐng)求的數(shù)據(jù)庇茫。
你可能好奇為什么不能丟棄控制器港粱,在一個(gè)類(lèi)里實(shí)現(xiàn)模型和視圖,這樣看起來(lái)更簡(jiǎn)單點(diǎn)旦签。
所有這一切都是為了代碼分離和可重用性查坪。理想上,視圖應(yīng)該完全和模型分離宁炫,如果視圖不依賴(lài)于一個(gè)特殊的模型的實(shí)現(xiàn)偿曙,那么他就可以用于不通的模型,展示一些其他的數(shù)據(jù)羔巢。
例如望忆,如果未來(lái)你也想要添加電影或者書(shū)籍,你可以仍然使用AlbumView來(lái)展示你的電影和書(shū)籍對(duì)象竿秆。此外启摄,如果你想創(chuàng)建一個(gè)新的項(xiàng)目,其中有一些與albums有關(guān)幽钢,你可以簡(jiǎn)單的重用你的Album類(lèi)歉备,因?yàn)樗腿魏我晥D都無(wú)關(guān)。這就是MVC的魔力匪燕。
怎么使用MVC模式
首先蕾羊,你需要確保在你的項(xiàng)目里每一個(gè)類(lèi)都可以劃分到模型喧笔,視圖,控制器中的一個(gè)龟再。不要在一個(gè)類(lèi)里包含兩種不同的角色溃斋。到目前為止你已經(jīng)完成了一個(gè)好的開(kāi)始,創(chuàng)建了一個(gè)Album類(lèi)和一個(gè)AlbumView類(lèi)吸申。
然后梗劫,為了確保你按照這個(gè)方法工作,應(yīng)該創(chuàng)建三個(gè)項(xiàng)目組管理你的代碼截碴,一個(gè)對(duì)應(yīng)一個(gè)角色梳侨。
使用File\New\Group(或者快捷鍵Command+Option+N)
命名為Model,重復(fù)相同的過(guò)程創(chuàng)建View和Controller組日丹。
現(xiàn)在拖拽Album.h和Album.m到Model組走哺,拖拽AlbumView.h和AlbumView.m到View組,最后拖拽ViewController.h和ViewController.m到Controller組哲虾。
在這里點(diǎn)上項(xiàng)目目錄結(jié)構(gòu)應(yīng)該看起來(lái)像下面這樣:
沒(méi)有了那些雜亂無(wú)序的文件你的工程已經(jīng)開(kāi)起來(lái)好很多了丙躏。當(dāng)然了,你可以有其他的分組和類(lèi)束凑,但是應(yīng)用的核心是包含在這三個(gè)組中晒旅。
現(xiàn)在你的組件是有組織的了,你需要從某個(gè)地方得到唱片集數(shù)據(jù)汪诉。你將要?jiǎng)?chuàng)建一個(gè)API類(lèi)废恋,通過(guò)你的代碼管理這些數(shù)據(jù)——這正好給出了一個(gè)討論你的下一個(gè)設(shè)計(jì)模式的機(jī)會(huì)——單例。
單例模式
單例設(shè)計(jì)模式確保對(duì)于一個(gè)給定的類(lèi)只存在一個(gè)實(shí)例扒寄,有一個(gè)全局的路徑指向這個(gè)實(shí)例鱼鼓。當(dāng)?shù)谝淮涡枰獣r(shí)通常使用懶加載來(lái)創(chuàng)建單例。
注意:Apple使用了許多單例该编。例如[NSUserDefaults standardUserDefaults], [UIApplication sharedApplication], [UIScreen mainScreen], [NSFileManager defaultManager] 都返回一個(gè)單例對(duì)象迄本。
你可能想知道為什么你這么的在意一個(gè)類(lèi)會(huì)存在多于一個(gè)實(shí)例。代碼和內(nèi)存都是便宜的课竣,對(duì)不對(duì)嘉赎?
一些情況在一個(gè)類(lèi)有且僅有一個(gè)實(shí)例是有重要意義的。例如稠氮,不需要更多的Logger實(shí)例曹阔,除非你想要一次寫(xiě)幾個(gè)日志文件半开「襞或者,獲得一個(gè)全局的配置管理類(lèi):實(shí)現(xiàn)線程安全的訪問(wèn)一個(gè)單獨(dú)的共享資源寂拆,例如一個(gè)配置文件奢米,要比許多對(duì)象可能同時(shí)修改這個(gè)配置文件要簡(jiǎn)單的多得多抓韩。
怎么使用單例模式
來(lái)看一看下面的圖表:
上圖展示了一個(gè)Logger類(lèi),有一個(gè)單獨(dú)的屬性(是一個(gè)單獨(dú)的實(shí)例)鬓长,和兩個(gè)方法 sharedInstance 和init谒拴。
第一次一個(gè)客戶端發(fā)送sharedInstance消息,instance屬性還沒(méi)有稱(chēng)初始化涉波,因此你創(chuàng)建了這個(gè)類(lèi)的一個(gè)新的實(shí)例并且返回它的引用英上。
下一次你調(diào)用sharedInstance,instance不需要初始化馬上返回啤覆,這個(gè)邏輯保證了一直都是只有一個(gè)實(shí)例存在苍日。
你將要通過(guò)創(chuàng)建一個(gè)單例類(lèi)管理所有的唱片集數(shù)據(jù)來(lái)實(shí)現(xiàn)這個(gè)模式。
你注意到了在項(xiàng)目里有一個(gè)名為API的組窗声,這里面放了為你的app提供服務(wù)的所有類(lèi)相恃。在這個(gè)組里使用 iOS\Cocoa Touch\Objective-C class 模板創(chuàng)建一個(gè)新的類(lèi),名字為L(zhǎng)ibraryAPI,父類(lèi)為NSObject笨觅。
打開(kāi)LibraryAPI.h,用下面的代碼替換它的內(nèi)容:
@interface LibraryAPI : NSObject
+ (LibraryAPI*)sharedInstance;
@end
現(xiàn)在去LibraryAPI.m,在@implementation這一行的后面插入下面的方法:
+ (LibraryAPI*)sharedInstance
{
// 1
static LibraryAPI *_sharedInstance = nil;
// 2
static dispatch_once_t oncePredicate;
// 3
dispatch_once(&oncePredicate, ^{
_sharedInstance = [[LibraryAPI alloc] init];
});
return _sharedInstance;
}
在這個(gè)短短的方法里發(fā)生了許多事情:
- 聲明了一個(gè)靜態(tài)變量來(lái)持有類(lèi)的實(shí)例拦耐,確保在類(lèi)中實(shí)例一直是有效的。
- 聲明靜態(tài)變量 dispatch_once_t 见剩,確保初始化代碼僅執(zhí)行一次杀糯。
- 使用 Grand Central Dispatch(GCD)執(zhí)行語(yǔ)句塊,實(shí)例化一個(gè)LibraryAPI的實(shí)例苍苞。單例設(shè)計(jì)模式的實(shí)質(zhì):一旦一個(gè)類(lèi)已經(jīng)實(shí)例化火脉,就再也不會(huì)被再次的調(diào)用。
下一次你調(diào)用sharedInstance方法柒啤,在dispatch_once塊里面的代碼將不會(huì)執(zhí)行(因?yàn)橐呀?jīng)執(zhí)行過(guò)一次了)倦挂,你得到的是之前創(chuàng)建的LibraryAPI實(shí)例的引用。
注意:想要學(xué)習(xí)更多的關(guān)于GCD的知識(shí)担巩,請(qǐng)?jiān)谖覀兊木W(wǎng)站查看教程Multithreading and Grand Central Dispatch和How to Use Blocks
現(xiàn)在你有了單例對(duì)象來(lái)管理唱片集方援,更進(jìn)一步的創(chuàng)建一個(gè)類(lèi)控制數(shù)據(jù)的持久化存儲(chǔ)。
使用 iOS\Cocoa Touch\Objective-C class 模板在API組里創(chuàng)建一個(gè)新類(lèi)涛癌,命名為PersistencyManager犯戏,父類(lèi)NSObject。
打開(kāi)PersistencyManager.h,在文件頭部添加下面的import語(yǔ)句:
#import "Album.h"
接下來(lái)拳话,在PersistencyManager.h文件@interface這行的后面添加下面的代碼:
- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;
上面是你需要操作Album數(shù)據(jù)的三個(gè)方法的聲明先匪。
打開(kāi)PersistencyManager.m文件在@implementation后面添加下面的代碼:
@interface PersistencyManager () {
// an array of all albums
NSMutableArray *albums;
}
上面的代碼添加了一個(gè)類(lèi)的擴(kuò)展,這是另一個(gè)為類(lèi)添加私有方法和變量的途徑弃衍,外部的類(lèi)將不會(huì)知道它們呀非。在這,你定義了一個(gè)可變數(shù)組去存儲(chǔ)album數(shù)據(jù),使添加和刪除數(shù)據(jù)變得容易許多岸裙。
現(xiàn)在在PersistencyManager.m文件@implementation后面添加下面的代碼:
- (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中你用5個(gè)例子albums填充了數(shù)組猖败,如果上面的唱片集不是你喜歡的,用你喜歡的音樂(lè)替換他們是一件很酷的事情降允。:]
現(xiàn)在添加下面三個(gè)方法到PersistencyManager.m中去恩闻。
- (NSArray*)getAlbums
{
return albums;
}
- (void)addAlbum:(Album*)album atIndex:(int)index
{
if (albums.count >= index)
[albums insertObject:album atIndex:index];
else
[albums addObject:album];
}
- (void)deleteAlbumAtIndex:(int)index
{
[albums removeObjectAtIndex:index];
}
這些方法允許你獲取,添加和刪除albums剧董。
編譯你的項(xiàng)目確保一切事情順利進(jìn)行幢尚。
在這一部分,你可能想知道為什么要有PersistencyManager這個(gè)類(lèi)翅楼,它并不是一個(gè)單例呀侠草。PersistencyManager和LibraryAPI之間的關(guān)系將會(huì)在下一個(gè)章節(jié)揭秘,在那里你將會(huì)看到外觀設(shè)計(jì)模式犁嗅。
外觀設(shè)計(jì)模式
外觀設(shè)計(jì)模式為一個(gè)復(fù)雜的子系統(tǒng)提供一個(gè)單一的接口边涕。你只需要暴漏一個(gè)簡(jiǎn)單地統(tǒng)一的API,取代向用戶暴漏一組類(lèi)和它們的APIs褂微。
下圖解釋了這個(gè)概念:
API的使用者完全不知道下層的復(fù)雜程度功蜓。當(dāng)處理大量的類(lèi),特別是使用這些類(lèi)起來(lái)很復(fù)雜或者特別難理解的時(shí)候宠蚂,使用外觀設(shè)計(jì)模式是理想的選擇式撼。
外觀設(shè)計(jì)模式將使用系統(tǒng)的代碼進(jìn)行解耦,將類(lèi)的接口和實(shí)現(xiàn)對(duì)你隱藏求厕,在子系統(tǒng)的內(nèi)部工作中減少了對(duì)外部代碼的依賴(lài)著隆,外觀下面的類(lèi)可能會(huì)修改,當(dāng)改變發(fā)生在幕后時(shí)外觀仍舊保持相同的API呀癣。
例如美浦,如果有一天你想要替換你的后臺(tái)服務(wù),你不需要改變使用你PAI的代碼项栏,因?yàn)樗麄儗⒉粫?huì)改變浦辨。
一般的,你有PersistencyManager保存album數(shù)據(jù)到本地沼沈,并且HTTPClient用來(lái)操控遠(yuǎn)程交流流酬,在你的項(xiàng)目里的其他的類(lèi)不應(yīng)該知道這些邏輯。
為了實(shí)現(xiàn)這個(gè)模式列另,LibraryAPI僅僅需要持有PersistencyManager和HTTPClient的實(shí)例芽腾。
然后,LibraryAPI需要提供一些簡(jiǎn)單的API來(lái)訪問(wèn)這些服務(wù)页衙。
注意:通常摊滔,一個(gè)單例存在app的整個(gè)生命周期。你不應(yīng)該在單例中持有太多的對(duì)象的強(qiáng)引用,因?yàn)樗鼈冎钡絘pp關(guān)閉才會(huì)被釋放惭载。
這個(gè)設(shè)計(jì)看起來(lái)像下面的樣子:
LibraryAPI將會(huì)暴漏給其他的代碼旱函,但是將會(huì)隱藏HTTPClient和PersistencyManager的復(fù)雜部分响巢。
打開(kāi)LibraryAPI.h 在文件頂部添加import語(yǔ)句:
#import "Album.h"
然后描滔,添加下面的方法定義到LibraryAPI.h中
- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;
到現(xiàn)在,你將會(huì)暴漏這些方法給其他的類(lèi)踪古。
去LibraryAPI.m文件含长,添加下面的兩條import語(yǔ)句:
#import "PersistencyManager.h"
#import "HTTPClient.h"
這是唯一的引入這些類(lèi)的地方。記住了:你的API將會(huì)是訪問(wèn)你的"復(fù)雜"系統(tǒng)的唯一的途徑伏穆。
現(xiàn)在拘泞,通過(guò)類(lèi)的擴(kuò)展添加一些私有變量(在@implementation這行的上面):
@interface LibraryAPI () {
PersistencyManager *persistencyManager;
HTTPClient *httpClient;
BOOL isOnline;
}
@end
isOnline 決定了任何的album數(shù)據(jù)的修改是否應(yīng)該更新服務(wù),例如添加或者刪除數(shù)據(jù)枕扫。
你現(xiàn)在應(yīng)該通過(guò)init方法初始化這些變量陪腌,添加下面的代碼到LibraryAPI.h中:
- (id)init
{
self = [super init];
if (self) {
persistencyManager = [[PersistencyManager alloc] init];
httpClient = [[HTTPClient alloc] init];
isOnline = NO;
}
return self;
}
HTTP 客戶端不會(huì)真正的與一個(gè)服務(wù)器工作,這里只是用來(lái)展示外觀模式的使用烟瞧,所以isOnline總是NO诗鸭。:]
接下來(lái),添加下面三個(gè)方法到LibraryAPI.m
- (NSArray*)getAlbums
{
return [persistencyManager getAlbums];
}
- (void)addAlbum:(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:.這個(gè)類(lèi)第一次本地的更新數(shù)據(jù)参滴,如果有網(wǎng)絡(luò)連接强岸,通過(guò)遠(yuǎn)程服務(wù)進(jìn)行更新。這就是外觀的真正力量砾赔,當(dāng)你系統(tǒng)外的某個(gè)類(lèi)添加一個(gè)新的album時(shí)蝌箍,它不知道——當(dāng)然也不需要知道——下層的復(fù)雜性。
注意:當(dāng)在你的子系統(tǒng)需要設(shè)計(jì)一個(gè)外觀時(shí)暴心,沒(méi)有什么可以組織客戶端直接訪問(wèn)這些"隱藏"類(lèi)妓盲,不要吝嗇防御代碼,不要想當(dāng)然的以為所有客戶端一定會(huì)用與使用外觀的方式一樣來(lái)使用你的類(lèi)专普。
編譯并運(yùn)行你的應(yīng)用本橙,你將會(huì)看到下面一些樣的激動(dòng)人心的空的黑屏幕。
你需要一些東西來(lái)讓屏幕展示出數(shù)據(jù)——使用下一個(gè)設(shè)計(jì)模式對(duì)你來(lái)說(shuō)很不錯(cuò):裝飾器脆诉。
裝飾器設(shè)計(jì)模式
裝飾器模式:在不改變對(duì)象代碼的情況下甚亭,為對(duì)象動(dòng)態(tài)的添加行為和職責(zé)。
在Objective-C中有兩個(gè)非常常見(jiàn)的這個(gè)模式的實(shí)現(xiàn):類(lèi)別和代理击胜。
類(lèi)別
類(lèi)別是一個(gè)十分強(qiáng)大的機(jī)制亏狰, 它允許你不需要子類(lèi)化就可以為已存在的類(lèi)添加方法。這個(gè)新的方法在編譯時(shí)被添加偶摔,可以像其他方法一樣被執(zhí)行暇唾。它和裝飾器的定義有稍許的不同,類(lèi)別不能持有擴(kuò)展類(lèi)的實(shí)例。
注意:不但可以擴(kuò)展你自己的類(lèi)策州,而且你可以給任何Cocoa類(lèi)添加方法瘸味。
如何使用類(lèi)別
想象一下這種情況,你有一個(gè)album對(duì)象够挂,想要把它展示在一個(gè)tableview里面:
album的標(biāo)題從哪里來(lái)旁仿?Album是一個(gè)模型對(duì)象,它不關(guān)系你怎么展示這些數(shù)據(jù)孽糖。你需要一些額外的代碼枯冈,添加方法到Album類(lèi),但是不能直接修改類(lèi)的內(nèi)容办悟。
你將創(chuàng)建一個(gè)擴(kuò)展子Album的類(lèi)別尘奏,它定義了一個(gè)新方法,這個(gè)方法返回一個(gè)可以很容易被tabieview使用的數(shù)據(jù)結(jié)構(gòu)病蛉。
這個(gè)數(shù)據(jù)結(jié)構(gòu)看起來(lái)像下面的樣子:
添加Album的一個(gè)類(lèi)別炫加,使用 File\New\File... 選擇 Objective-C category 模板——不要選擇Objective-C class!在Category中輸入 TableRepresentation并且在Category on 中輸入Album铺然。
注意:你注意到了新文件的名字了嗎俗孝?Album+TableRepresentation表示你擴(kuò)展了Album類(lèi)。這個(gè)約定是重要的探熔,因?yàn)樗侨菀组喿x的并且阻止了與你或者其他人可能創(chuàng)建其他的類(lèi)別產(chǎn)生沖出驹针。
進(jìn)入Album+TableRepresentation.h文件,添加下面的方法聲明:
- (NSDictionary*)tr_tableRepresentation;
看到在方法名的前面有tr_前綴诀艰,作為類(lèi)別名字的縮寫(xiě):TableRepresentation柬甥。同樣的,像這樣的約定可以阻止與其他的方法產(chǎn)生沖出其垄。
注意:如果類(lèi)別中方法的名字與原始類(lèi)中方法的名字一樣苛蒲,或者這這個(gè)類(lèi)的另一個(gè)類(lèi)別中有一樣名字的方法(或者甚至是一個(gè)父類(lèi)),在運(yùn)行時(shí)哪一個(gè)方法被調(diào)用時(shí)不明確的绿满。如果你對(duì)自己的類(lèi)使用類(lèi)別臂外,這種問(wèn)題是微乎其微的,但是當(dāng)你對(duì)標(biāo)準(zhǔn)的Cocoa 或者Cocoa Touch類(lèi)使用類(lèi)別添加方法是就會(huì)出現(xiàn)一系列的問(wèn)題喇颁。
進(jìn)入Album+TableRepresentation.m添加下面的方法:
- (NSDictionary*)tr_tableRepresentation
{
return @{@"titles":@[@"Artist", @"Album", @"Genre", @"Year"],
@"values":@[self.artist, self.title, self.genre, self.year]};
}
仔細(xì)想想這個(gè)模式的強(qiáng)大之處是什么:
- 你可以從Album直接使用功能漏健。
- 你給Album類(lèi)添加了東西但是沒(méi)有子類(lèi)化它,如果你需要?jiǎng)?chuàng)建Album的子類(lèi)橘霎,你仍然可以那樣做蔫浆。
- 你沒(méi)有修改Album的任何代碼,就讓你得到了一個(gè)Album可以展示在tableview中的數(shù)據(jù)格式姐叁。
蘋(píng)果公司在基礎(chǔ)類(lèi)中大量的使用了類(lèi)別瓦盛,看看他們是怎么做的洗显,打開(kāi)NSString.h。發(fā)現(xiàn)@interface NSString原环,你將會(huì)看到這個(gè)類(lèi)定義了三個(gè)類(lèi)別:NSStringExtensionMethods, NSExtendedStringPropertyListParsing and NSStringDeprecated挠唆。類(lèi)別幫助你讓方法有組織并且分成了許多部分。
代理
另一個(gè)裝飾器設(shè)計(jì)模式嘱吗,代理玄组,是這樣一種機(jī)制:一個(gè)對(duì)象的行為代表或者協(xié)調(diào)另一個(gè)對(duì)象。例如柜与,當(dāng)你使用TableView巧勤,你必須實(shí)現(xiàn)方法:tableView:numberOfRowsInSection:嵌灰。
你不可能期待著TableView知道每一節(jié)會(huì)有多少行弄匕,因?yàn)檫@是程序特有的。因此沽瞭,計(jì)算每一節(jié)有多少行的任務(wù)就交給了TableView 的代理迁匠。這允許TableView的展示不依賴(lài)于數(shù)據(jù)。
下面是一個(gè)模擬的過(guò)程驹溃,解釋了當(dāng)你新建一個(gè)TableView時(shí)都放生了什么:
TableView對(duì)象做它的工作去展示一個(gè)列表城丧。然而,它終究需要一些它沒(méi)有的信息豌鹤,此時(shí)亡哄,它向它的代理發(fā)送消息詢(xún)問(wèn)額外的信息。在Objective-C的代理模式的實(shí)現(xiàn)中布疙,一個(gè)類(lèi)可以通過(guò)協(xié)議聲明可選和必須的方法蚊惯,稍后你將會(huì)了解協(xié)議是什么。
子類(lèi)化一個(gè)對(duì)象然后重寫(xiě)必要的方法可能看起來(lái)更容易些灵临,但是考慮到你只能子類(lèi)化一個(gè)單一的類(lèi)截型,如果你想一個(gè)對(duì)象是兩個(gè)或者更多對(duì)象的代理,通過(guò)子類(lèi)化是不可能實(shí)現(xiàn)的儒溉。
注意:這是一個(gè)重要的模式宦焦,蘋(píng)果公司在很多的UIKit類(lèi)中使用它:UITableView, UITextView, UITextField, UIWebView, UIAlert, UIActionSheet, UICollectionView, UIPickerView, UIGestureRecognizer, UIScrollView。還遠(yuǎn)遠(yuǎn)不止這些顿涣。
如何使用代理模式
打開(kāi)ViewController.m文件波闹,在文件頂部添加下面的兩條import語(yǔ)句:
#import "LibraryAPI.h"
#import "Album+TableRepresentation.h"
現(xiàn)在,添加這些私有變量到這個(gè)類(lèi)的擴(kuò)展中涛碑,類(lèi)擴(kuò)展看起來(lái)像下面的樣子:
@interface ViewController () {
UITableView *dataTable;
NSArray *allAlbums;
NSDictionary *currentAlbumData;
int currentAlbumIndex;
}
@end
然后精堕,在類(lèi)的擴(kuò)展中用下面這行替換掉@interface這一行:
@interface ViewController () <UITableViewDataSource, UITableViewDelegate> {
這是使你的代理遵守協(xié)議——把它想成是一個(gè)履行方法的合同的代理的承諾。這里锌唾,你指出了ViewController將要遵守UITableViewDataSource和UITableViewDelegate 協(xié)議锄码。TableView要絕對(duì)的保證確保必須的方法被它的代理實(shí)現(xiàn)夺英。
下一步,用下面的方法替換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 sharedInstance] getAlbums];
// 3
// the uitableview that presents the album data
dataTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:UITableViewStyleGrouped];
dataTable.delegate = self;
dataTable.dataSource = self;
dataTable.backgroundView = nil;
[self.view addSubview:dataTable];
}
對(duì)上面的代碼進(jìn)行分解:
- 改變背景顏色為好看的深藍(lán)色滋捶。
- 通過(guò)API得到一組albums痛悯,你沒(méi)有直接使用PersistencyManager。
- 創(chuàng)建TableView重窟,指明ViewController是TableView的代理和數(shù)據(jù)源载萌。所以,ViewController將提供所有TableView需要的信息巡扇。
現(xiàn)在扭仁,添加下面的方法到ViewCont.m中:
- (void)showDataForAlbumAtIndex:(int)albumIndex
{
// defensive code: make sure the requested index is lower than the amount of albums
if (albumIndex < allAlbums.count)
{
// fetch the album
Album *album = allAlbums[albumIndex];
// save the albums data to present it later in the tableview
currentAlbumData = [album tr_tableRepresentation];
}
else
{
currentAlbumData = nil;
}
// we have the data we need, let's refresh our tableview
[dataTable reloadData];
}
showDataForAlbumAtIndex: 方法從數(shù)組中獲取需要的數(shù)據(jù),當(dāng)你想要展示新的數(shù)據(jù)厅翔,只需要調(diào)用reloadData方法乖坠。使TableView請(qǐng)求代理下面這些事情:這個(gè)列表有多少個(gè)節(jié),每個(gè)節(jié)有多少行以及每行應(yīng)該長(zhǎng)什么樣子刀闷。
在viewDidLoad的最后添加下面的代碼:
[self showDataForAlbumAtIndex:currentAlbumIndex];
在app開(kāi)始時(shí)加載通用數(shù)據(jù)熊泵,由于currentAlbumIndex 被初始化為0,將展示集合中的第一個(gè)album甸昏。
編譯并運(yùn)行你的項(xiàng)目顽分,程序?qū)?huì)崩潰,在調(diào)試控制臺(tái)出現(xiàn)下面的異常信息施蜜。
這是怎么了卒蘸?你聲明了ViewController為T(mén)ableView的代理和數(shù)據(jù)源,但是翻默,這樣做了你必須遵守所有必須的方法——包括 tableView:numberOfRowsInSection:——你還沒(méi)有實(shí)現(xiàn)缸沃。
在@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)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
}
cell.textLabel.text = currentAlbumData[@"titles"][indexPath.row];
cell.detailTextLabel.text = currentAlbumData[@"values"][indexPath.row];
return cell;
}
tableView:numberOfRowsInSection:返回在列表中要展示的行數(shù),這個(gè)數(shù)字是在數(shù)據(jù)結(jié)構(gòu)中的標(biāo)題數(shù)冰蘑。
tableView:cellForRowAtIndexPath: 用標(biāo)題和它的值創(chuàng)建并返回一個(gè)cell和泌。
編譯并運(yùn)行你的項(xiàng)目,你的app應(yīng)該正常運(yùn)行并且像下面的樣子:
到現(xiàn)在事情看上去很好祠肥,但是如果你重新調(diào)用app中的第一張圖片武氓,在頂端有一個(gè)水平的滑動(dòng)。編寫(xiě)一個(gè)用途單一的水平滑動(dòng)仇箱,為什么不讓它對(duì)其他視圖也可以重用呢县恕。
為了使視圖可重用,所有的關(guān)于內(nèi)容的決定都應(yīng)該交給另一個(gè)對(duì)象:一個(gè)代理剂桥。水平滑動(dòng)應(yīng)該定義方法忠烛,代理實(shí)現(xiàn)這些方法完成協(xié)同工作。和TableView的代理工作原理的很像权逗。當(dāng)我們討論下一個(gè)設(shè)計(jì)模式的時(shí)候美尸,我們將實(shí)現(xiàn)這個(gè)功能冤议。