相信做app開發(fā)的同學(xué),對于一些第三方的統(tǒng)計分析、錯誤收集等sdk應(yīng)該都不陌生。就目前而言市面上也有許多相同功能的產(chǎn)品,眼花繚亂,讓人無法抉擇選哪一款sdk才是最靠譜的。那就隨便先選一款試試用吧!
那么問題來了:如果項目都快做完了結(jié)果發(fā)現(xiàn)這款sdk實在坑爹,不僅擴展性差,還經(jīng)常讓app crash,那你是不是會想到替換掉這個sdk?
ok,那我們就換另一個試試,下載sdk下來,一看,傻眼了,設(shè)計風(fēng)格,封裝模塊完全不一樣,于是乎我們就到項目中全局搜索找到之前的sdk代碼干掉,然后重新再到各種地方用新的sdk來寫新的邏輯來替換,關(guān)鍵的是,中間還不知道會產(chǎn)生多少bug,漏掉多少未修改的代碼,總之始終會有一種不靠譜的感覺。
換一次還算好的,如果之后團隊壯大了,這些數(shù)據(jù)分析之類的東西突然想自己做了,畢竟這些有價值的數(shù)據(jù)并不想這么拱手讓給一個第三方的公司嘛~這個時候你是不是就只想說:『呵呵』
所以這個時候適配器模式就起到作用了~
何為適配器模式
gof對于適配器模式的解釋如下:
將一個類的接口轉(zhuǎn)換成客戶希望的另外一個接口。adapter模式使得原本由于接口不兼容而不能一起工作的那些類可以在一起工作。
個人通俗理解:
適配器:顧名思義,將不兼容的轉(zhuǎn)換為兼容,如電源適配器,將全世界各種不相同的電壓轉(zhuǎn)換成相同的電壓輸出給目標(biāo)設(shè)備。
這里可以將目標(biāo)設(shè)備理解為『接口』,世界各種電壓可以理解為『產(chǎn)生相同功能的類』,電源適配器可以理解為『需要實現(xiàn)的適配器類』。
適配器模式產(chǎn)生的效果是:在不修改代碼或者修改極少代碼的情況下,快速的切換源(數(shù)據(jù)源、內(nèi)容源等)。
就像電源適配器一樣,去到不同國家,同一個設(shè)備只需要不同的電源適配器就可以使用當(dāng)前國家的電源,而不需要取拆卸機器。
使用真實場景
如文章開頭所講,被某盟的sdk坑了之后(確實在某些狀況下讓app crash,產(chǎn)生原因初步判斷是濫用performselector,不考慮對象被釋放的情況而產(chǎn)生的crash),產(chǎn)生替換念想而思考,如果將來替換豈不是又要苦逼我們自己?
于是乎為了將來的輕松就必須動動腦子去設(shè)計代碼了,于是有了今天的適配器模式實戰(zhàn)。
如何使用適配器模式
一個適配器允許接口不兼容的類在一起工作。它把它自己包裹成一個對象,公開一個與這個對象相互作用的標(biāo)準接口。
如果你熟習(xí)適配器模式,你會注意到蘋果實施它的時候有一點不同的習(xí)慣─蘋果使用協(xié)議 (protocols)。你可能熟習(xí)像 uitableviewdelegate, uiscrollviewdelegate, nscoding 和 nscopying 這樣的協(xié)議。例子,nscopying 的協(xié)議 (protocol),任何類都可以提供這樣一個標(biāo)準的復(fù)制方法。
我們提到的滾動區(qū)域是這樣的:
現(xiàn)在開始,在項目導(dǎo)航的 view 文件夾上右擊鼠標(biāo),選擇 new file…,用 ios\cocoa touch\object-c class 模板創(chuàng)建一個新類。新類的名字叫 horizontalscroller,選擇它的子類為 uiview。
打開 horizontalscroller.h 文件在 @end 后面插入如下代碼:
@protocol horizontalscrollerdelegate <nsobject>
// methods declaration goes in here
@end
這里定義一個 horizontalscrollerdelegate 名字的協(xié)議,它繼承于 nsobject 協(xié)議,同樣的這是繼承它父類的一個 objective-c 類。符合 nsobject 協(xié)議,這是一個很好的做法─或者遵照 nsobject 協(xié)議。這能使你從定義的 nsobject 發(fā)送消息到 horizontalscroller 的代理。你將會看到為什么這很重要。
定義個代理執(zhí)行的方法,要在 @protocol 和 @end 之間,它們分為必要方法和可選方法。添加下面協(xié)議方法:
@required
// 詢問 delegate 在滾動區(qū)域里有多少個視圖要被顯示
- (nsinteger)numberofviewsforhorizontalscroller: (horizontalscroller*)scroller;
// 返回索引是 index 的視圖
- (uiview*)horizontalscroller:(horizontalscroller*)scroller viewatindex:(int)index;
// 當(dāng)索引是 index 的視圖被點擊了,通知 delegate
- (void)horizontalscroller:(horizontalscroller*)scroller clickedviewatindex:(int)index;
@optional
// 通知 delegate,顯示初始化時索引是 index 的視圖。這個方法是可選的
// ask the delegate for the index of the initial view to display. this method is optional
// 如果沒有被 delegate 執(zhí)行,默認值是 0
- (nsinteger)initialviewindexforhorizontalscroller:(horizontalscroller*)scroller;
這里我們必選的和可選的方法我們都定義了。必選方法一定要被代理執(zhí)行,它通常包含一些類必須要執(zhí)行的數(shù)據(jù)。這里,必選方法是獲取視圖的數(shù)量,當(dāng)前顯示視圖的索引和當(dāng)視圖被點擊的時候執(zhí)行的操作。可選方法這里是初始化視圖;如果沒有執(zhí)行 horizontalscroller 將會顯示第一個索引的視圖。
接下來,你需要在 horizontalscroller 內(nèi)部定義你的新代理。但是協(xié)議的定義在類的定義下面,因此在這點上它是不可見的。你該怎么辦?
解決辦法就是在前面聲明協(xié)議以便于編譯器(和xcode)知道這個協(xié)議是可用的。好了,在 @interface 上面加入下面代碼:
[/ode]
@protocol horizontalscrollerdelegate;
[/code]
還是 horizontalscroller.h,在 @interface 和 @end 之間加入下面代碼:
@property (weak) id<horizontalscrollerdelegate> delegate;
- (void)reload;
這個屬性被定義成為一個 weak。這是為了防止循環(huán) retain。如果一個類保持一個強指針(strong pointer)指向它的委托(delegate),同時委托也保持一個強指針指向這個類,在釋放類所占用的內(nèi)存時會造成 app 內(nèi)存泄漏。
id 的意思是把這個代理指定給一個類,它遵照 horizontalscrollerdelegate,給你一些類型安全。
reload 方法是模仿 uitableview 類的 relaoddata;它重新加載所有數(shù)據(jù)用來創(chuàng)建一個水平移動視圖。
用下面代碼替換 horizontalscroller.m 的內(nèi)容:
#import “horizontalscroller.m”
#define view_padding 10
#define view_dimensions 100
#define view_offset 100
@interface horizontalscroller () <uiscrollviewdelegate>
@end
@implementation horizontalscroller
{
uiscrollview *scroller;
}
@end
來解釋下每塊代碼:
常量定義,在設(shè)計時間可以方便修改布局。在滾動視圖內(nèi),每個圖片的大小在一個 100×100 內(nèi)邊距為 10 點(point) 的矩形內(nèi)。
horizontalscroller 遵照 uiscrollviewdelegate 協(xié)議。因為 horizontalscroller 使用一個 uiscrollview 來滾動專輯封面,它需要知道用戶什么時候停止?jié)L動。
創(chuàng)建一個包含圖片的滾動視圖。
接下來你需要執(zhí)行初始化。添加下面的方法:
- (id)initwithframe:(cgrect)frame
{
self = [super initwithframe:frame];
if (self) {
scroller = [[uiscrollerview alloc] initwithframe:cgrectmake(0, 0, frame.size.width, frame.size.height)];
scroller.delegate = self;
uitapgesturerecognizer *taprecognizer = [[uitapgesturerecognizer alloc] initwithtarger:self action:@select(scrollertapped:)];
[scroller addgesturerecognizer:taprecognizer];
}
return self;
}
horizontalscroller 將被滾動視圖整個填充。如果一個專輯封面被點擊,uitapgesturerecognizer 將會監(jiān)聽它上面的事件。如果有,它會通知 horizontalscroller 的代理。
現(xiàn)在添加下面方法:
- (void)scrollertapped:(uitapgesturerecognizer*)gesture
{
cgpoint location = [gesture locationinview:gesture.view];
// we can't use an enumerator here, because we don't want to enumerate over all of the uiscrollview subviews.
// we want to enumerate only the subview that we added
for (int index=0; index<[self.delegate numberofviewforhorizontalscroller:self]; index++) {
uiview *view = scroller.subviews[index];
if (cgrectcontainspoint(view.frame, location)) {
[self.delegate horizontalscroller:self clickedviewatindex:index];
[scroller setcontentoffset:cgpointmake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0) animated:yes];
break;
}
}
}
手勢操作就如同傳入的一個參數(shù),可以從 locationinview: 獲取定位信息。
接下來,調(diào)用委托的 numberofviewforhorizontalscroller: 方法。它必須遵照 horizontalscrollerdelegate 的協(xié)議安全發(fā)送消息,否則 horizontalscroller 實例的代理是沒法使用這些信息。
滾動視圖里的每個視圖,用 cgrectcontainspoint 執(zhí)行一個點擊測試,找到那個被點擊的視圖。當(dāng)視圖被找到,發(fā)送給委托一個消息 horizontalscroller:clickedviewatindex:。當(dāng)你跳出這個循環(huán)后,設(shè)置被點擊的視圖滾動到視圖中間。
現(xiàn)在添加下面的代碼,用來刷新滾動視圖(scroller):
- (void)reload
{
// 1 - nothing to load if there's no delegate
if (self.delegate == nil) return;
// 2 - remover all subviews
[scroller.subviews enumerateobjectsusingblock:^(id obj, nsuinteger idx, bool *stop) {
[obj removefromsuperview];
}
// 3 - xvalue is the starting point of the views inside the scroller
cgfloat xvalue = views_offset;
for (int i=0; i<[self.delegate numberofviewsforhorizontalscroller:self]; i++) {
// 4 - add a view at the right position
xvalue += view_padding;
uiview *view = [self.delegate horizontalscroller:self viewatindex:i]
view.frame = cgrectmake(xvalue, view_padding, view_dimensions, view_dimensions);
xvalue += view_dimensions + view_padding;
}
// 5
[scroller setcontentsize:cgsizemake(xvalue+views_offset, self.frame.size.height)];
// 6 - if an initial view is defined, center the scroller on it
if (self.delegate respondstoselector:@select(initialviewindexforhorizontalscroller:)]) {
int initialview = [self.delegate initialviewindexforhorizontalscroller:self];
[scroller setcontentoffset:cgpointmake(initialview*(view_dimensions+(2*view_padding)), 0) animated:yes];
}
}
能過代碼一步步來討論:
如果沒有代理,這里什么事情也不做。
移除之前添加的所有的子視圖。
給所有視圖設(shè)置一個偏移(offset)位置。現(xiàn)在的是 100,但是通過頂部的 #define,它很容易修改。
horizontalscroller 通過它的委托一次請求一個視圖,用之前定義的 padding 值把它們依次的一個個放置下來。
當(dāng)所有的視圖都生成好,通過設(shè)置滾動視圖內(nèi)容的偏移量以達到用戶能過滾動可以看到所有專輯封面的目的。
horizontalscroller 的委托需要驗證是否響應(yīng)了 initialviewindexforhorizontalscroller: 方法。這個驗證是必需的,因為這個特別的協(xié)議方法是可選性的。如果代理沒有執(zhí)行這個方法,它的默認值會是 0。最終,通過委托,這塊代碼會在滾動視圖中間設(shè)置一個初始化好的視圖。
當(dāng)數(shù)據(jù)發(fā)生改變的時候執(zhí)行 reload 方法。當(dāng)添加 horizontalscroller 到別個一個視圖時,你同樣可以執(zhí)行這個方法。在 horizontalscroller.m 添加下面的代碼替換后面的方案:
- (void)didmovetosuperview
{
[self reload];
}
當(dāng)它要添加一個子視圖的時候,didmovetosuperview 會發(fā)送消息給視圖。這時正好可以更新滾動視圖的內(nèi)容。
horizontalscroller 的最后一個難題就是,如何設(shè)置你看到的專輯總是在滾動視圖的中間。為了這些,當(dāng)用戶通過他們的手指拖動滾動視圖的時候你就需要做一些計算了。
添加下面方法(同樣在 horizontalscroller.m):
- (void)centercurrentview {
int xfinal = scroller.contentoffset.x + (views_offset/2) + view_padding;
int viewindex = xfinal / (view_dimensions + (2*view_padding));
xfinal = viewindex * (view_dimensions+(2*view_padding));
[scroller setcontentoffset:cgpointmake(xfinal, 0) animated:yes];
[self.delegate horizontalscroller:self clickedviewatindex:viewindex];
}
上面的代碼通過滾動視圖的當(dāng)前偏移量,外觀尺寸,內(nèi)邊距來計算當(dāng)前視圖離中心的距離。最后一行非常重要:當(dāng)一個視圖居中后,你需要通知委托你選擇的視圖改變了。
為了偵測用戶在滾動視圖內(nèi)完成拖拽的動作,你需要添加 uiscrollviewdelegate 方法:
- (void)scrollviewdidenddragging:(uiscrollview *)scrollview willdecelerate:(bool)decelerate
{
if (!decelerate)
{
[self centercurrentview];
}
}
- (void)scrollviewdidenddecelerating:(uiscrollview *)scrollview
{
[self centercurrentview];
}
當(dāng)用戶完成拖拽的時候 scrollviewdidenddragging:willdecelerate: 通知委托。如果滾動視圖沒有停止?jié)L動, decelerate 參數(shù)會返回 true。當(dāng)滾動結(jié)束,系統(tǒng)將會調(diào)用 scrollviewdidenddecelerating。當(dāng)用戶拖動滾動當(dāng)前視圖后,兩種情況,我們都需要調(diào)用一個新方法來使當(dāng)前視圖居中。
horizontalscroller 現(xiàn)在可以使用了。瀏覽你剛剛寫的代碼;這里沒有一處提到 album 和 albumview 類。這非常棒,說明這個新的滾動視圖是真正的完全獨立的和可重用的。
build 項目,確保所有的代碼編譯正確。
現(xiàn)在 horizontalscroller 完成了,是時候在你的 app 中使用了。打開 viewcontroller.m 添加如下引用:
#import “horizontalscroller.h”
#import “albumview.h”
給 viewcontroller 添加 horizontalscrollerdelegate:
@interface viewcontroller () <uitableviewdatasource, uitableviewdelegate, horizontalscroller>
在類的擴展里為水平滾動視圖添加如下實例變量:
horizontalscroller *scroller;
現(xiàn)在你可以執(zhí)行代理方法了;你會驚奇的發(fā)現(xiàn)只需要幾行代碼你就能實現(xiàn)很多功能。
在 viewcontroller.m 添加如下代碼:
#pragma mark - horizontalscrollerdelegate methods
- (void)horizontalscroller:(horizontalscroller *)scroller clickedviewatindex:(int)index
{
currentalbumindex = index;
[self showdataforalbumatindex:index];
}
這里設(shè)置一個變量用來存儲當(dāng)前的專輯,然后調(diào)用 showdataforalbumatindex: 顯示一個新專輯的數(shù)據(jù)。
提示:一般在方法代碼的前面放置 #pragma mark 指示符。編譯器會忽略這一行,當(dāng)你在使用 xcode 的跳轉(zhuǎn)工具欄(xcode's jump bar)查看你的方法列表時,你會看到一個分隔符和個加粗的指示標(biāo)題。在 xcode 里,這可以幫助你很容易的組織代碼。
下面,添加如下代碼:
- (nsinteger)numberofviewsforhorizontalscroller:(horizontalscroller *)scroller
{
return allalbums.count;
}
這里,協(xié)議方法返回滾動視圖里的視圖數(shù)量。因為滾動視圖需要顯示所有的專輯封面,這個 count 是所有專輯的數(shù)目。
現(xiàn)在,添加這些代碼:
- (uiview *)horizontalscroller:(horizontalscroller *)scroller viewatindex:(ini)index
{
album *album = allalbums[index];
return [[album alloc] initwithframe:cgrectmake(0, 0, 100, 100) albumcover:album.coverurl];
}
這里你創(chuàng)建了一個新 albumview,然后交給 horizontalscroller 使用。
就是這樣,通過三個這么短的方法就可以顯示一個漂亮的滾動視圖。
實際上,你仍需要創(chuàng)建一個真正的滾動視圖,然后添加到你的主視圖上,但是在這之前,先添加下面的方法:
- (void)reloadscroller
{
allalbums = [[libraryapi sharedinstance] getalbums];
if (currentalbumindex < 0) currentalbumindex = 0;
else if (currentalbumindex >=allalbum.count) currentalbumindex = allalbum.count - 1;
[scroller reload];
[self showdatafroalbumatindex:currentalbumindex;
}
這個方法從 libraryapi 加載專輯數(shù)據(jù),然后以當(dāng)前視圖的索引值為基礎(chǔ)設(shè)置顯示當(dāng)前的圖片。 如果當(dāng)前視圖的索引小于零,意味著當(dāng)前沒有選擇視圖,顯示列表里的第一張專輯。否則顯示最后一張專輯。
現(xiàn)在,在 viewdidload 里 [self showdataforalbumindex:0] 前面添加下面代碼來初始化滾動視圖:
scroller = [[horizontalscroller alloc] initwithframe:cgrectmake(0, 0, self.view.frame.size.width, 120)];
scroller.backgroundcolor = [uicolor colorwithred:0.24f greed:0.35f blue:0.49f alpha:1];
scroller.delegate = self;
[self.view addsubview:scroller];
[self reloadscroller];
上面的代碼創(chuàng)建了一個 horizontalscroller 的實例,設(shè)置了它的背景顏色和委托,添加滾動視圖到主視圖上,在滾動視圖的子視圖上加載專輯數(shù)據(jù)。
提示:如果一個協(xié)議變得很大,里面有很多方法,你應(yīng)該考慮把它們分散到幾個小的協(xié)議里去。uitableviewdelegate 和 uitableviewdatasource 就是一個很好的例子,因為它們都是 uitablveview 的協(xié)議。設(shè)計協(xié)議的時候,最好一個名稱引導(dǎo)一個功能。
構(gòu)建和運行你的項目,你會看到一個新的很了不起的水平滾動視圖:
啊嗯,等等。水平滾動的視圖已經(jīng)有了,可是專輯封面在哪里?
對了,你還沒有代碼來執(zhí)行下載圖片的功能。你需要添加一個下載圖片的方法。查檢 libraryapi 服務(wù)的所有接口,這里需要添加一個新的方法。不管怎樣,現(xiàn)在還有幾件事情需要考慮:
albumview 并沒沒有通過 libraryapi 立即工作。你沒有給視圖添加通信邏輯。
相同的原因,libraryapi 并不認識 albumview。
libraryapi 需要通知 albumview,一旦封面下載完成,albumview 就會顯示它。