相信做App開發的同學,對於一些第三方的統計分析、錯誤收集等SDK應該都不陌生。就目前而言市面上也有許多相同功能的產品,眼花繚亂,讓人無法抉擇選哪一款SDK才是最靠譜的。那就隨便先選一款試試用吧!
那麼問題來了:如果項目都快做完了結果發現這款SDK實在坑爹,不僅擴充性差,還經常讓App Crash,那你是不是會想到替換掉這個SDK?
OK,那我們就換另一個試試,下載SDK下來,一看,傻眼了,設計風格,封裝模組完全不一樣,於是乎我們就到項目中全域搜尋找到之前的SDK代碼幹掉,然後重新再到各種地方用新的SDK來寫新的邏輯來替換,關鍵的是,中間還不知道會產生多少bug,漏掉多少未修改的代碼,總之始終會有一種不靠譜的感覺。
換一次還算好的,如果之後團隊壯大了,這些資料分析之類的東西突然想自己做了,畢竟這些有價值的資料並不想這麼拱手讓給一個第三方的公司嘛~這個時候你是不是就只想說:『呵呵』
所以這個時候適配器模式就起到作用了~
何為適配器模式
GoF對於適配器模式的解釋如下:
將一個類的介面轉換成客戶希望的另外一個介面。Adapter模式使得原本由於介面不相容而不能一起工作的那些類可以在一起工作。
個人通俗理解:
適配器:顧名思義,將不相容的轉換為相容,如電來源配接器,將全世界各種不相同的電壓轉換成相同的電壓輸出給目標裝置。
這裡可以將目標裝置理解為『介面』,世界各種電壓可以理解為『產生相同功能的類』,電來源配接器可以理解為『需要實現的適配器類』。
適配器模式產生的效果是:在不修改代碼或者修改極少代碼的情況下,快速的切換源(資料來源、內容來源等)。
就像電來源配接器一樣,去到不同國家,同一個裝置只需要不同的電來源配接器就可以使用當前國家的電源,而不需要取拆卸機器。
使用真實情境
如文章開頭所講,被某盟的SDK坑了之後(確實在某些狀況下讓App Crash,產生原因初步判斷是濫用performSelector,不考慮對象被釋放的情況而產生的Crash),產生替換念想而思考,如果將來替換豈不是又要苦逼我們自己?
於是乎為了將來的輕鬆就必須動動腦子去設計代碼了,於是有了今天的適配器模式實戰。
如何使用適配器模式
一個適配器允許介面不相容的類在一起工作。它把它自己包裹成一個對象,公開一個與這個對象相互作用的標準介面。
如果你熟習適配器模式,你會注意到蘋果實施它的時候有一點不同的習慣─蘋果使用協議 (protocols)。你可能熟習像 UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying 這樣的協議。例子,NSCopying 的協議 (protocol),任何類都可以提供這樣一個標準的複製方法。
我們提到的捲動區域是這樣的:
現在開始,在項目導航的 View 檔案夾上右擊滑鼠,選擇 New File…,用 iOS\Cocoa Touch\Object-C class 模板建立一個新類。新類的名字叫 HorizontalScroller,選擇它的子類為 UIView。
開啟 HorizontalScroller.h 檔案在 @end 後面插入如下代碼:
複製代碼 代碼如下:
@protocol HorizontalScrollerDelegate <NSObject>
// methods declaration goes in here
@end
這裡定義一個 HorizontalScrollerDelegate 名字的協議,它繼承於 NSObject 協議,同樣的這是繼承它父類的一個 Objective-C 類。符合 NSObject 協議,這是一個很好的做法─或者遵照 NSObject 協議。這能使你從定義的 NSObject 發送訊息到 HorizontalScroller 的代理。你將會看到為什麼這很重要。
定義個代理執行的方法,要在 @protocol 和 @end 之間,它們分為必要方法和可選方法。添加下面協議方法:
複製代碼 代碼如下:
@required
// 詢問 delegate 在捲動區域裡有多少個視圖要被顯示
- (NSInteger)numberOfViewsForHorizontalScroller: (HorizontalScroller*)scroller;
// 返回索引是 index 的視圖
- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;
// 當索引是 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 執行,預設值是 0
- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;
這裡我們必選的和可選的方法我們都定義了。必選方法一定要被代理執行,它通常包含一些類必須要執行的資料。這裡,必選方法是擷取視圖的數量,當前顯示視圖的索引和當視圖被點擊的時候執行的操作。可選方法這裡是初始化視圖;如果沒有執行 HorizontalScroller 將會顯示第一個索引的視圖。
接下來,你需要在 HorizontalScroller 內部定義你的新代理。但是協議的定義在類的定義下面,因此在這點上它是不可見的。你該怎麼辦?
解決辦法就是在前面聲明協議以便於編譯器(和Xcode)知道這個協議是可用的。好了,在 @interface 上面加入下面代碼:
[/ode]
@protocol HorizontalScrollerDelegate;
[/code]
還是 HorizontalScroller.h,在 @interface 和 @end 之間加入下面代碼:
複製代碼 代碼如下:
@property (weak) id<HorizontalScrollerDelegate> delegate;
- (void)reload;
這個屬性被定義成為一個 weak。這是為了防止迴圈 retain。如果一個類保持一個強指標(strong pointer)指向它的委託(delegate),同時委託也保持一個強指標指向這個類,在釋放類所佔用的記憶體時會造成 app 記憶體流失。
id 的意思是把這個代理指定給一個類,它遵照 HorizontalScrollerDelegate,給你一些型別安全。
reload 方法是模仿 UITableView 類的 relaodData;它重新載入所有資料用來建立一個水平行動裝置檢視。
用下面代碼替換 HorizontalScroller.m 的內容:
複製代碼 代碼如下:
#import “HorizontalScroller.m”
#define VIEW_PADDING 10
#define VIEW_DIMENSIONS 100
#define VIEW_OFFSET 100
@interface HorizontalScroller () <UIScrollViewDelegate>
@end
複製代碼 代碼如下:
@implementation HorizontalScroller
{
UIScrollView *scroller;
}
@end
來解釋下每塊代碼:
常量定義,在設計時間可以方便修改布局。在滾動視圖內,每個圖片的大小在一個 100×100 內邊距為 10 點(point) 的矩形內。
HorizontalScroller 遵照 UIScrollViewDelegate 協議。因為 HorizontalScroller 使用一個 UIScrollView 來滾動專輯封面,它需要知道使用者什麼時候停止滾動。
建立一個包含圖片的滾動視圖。
接下來你需要執行初始化。添加下面的方法:
複製代碼 代碼如下:
- (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 將會監聽它上面的事件。如果有,它會通知 HorizontalScroller 的代理。
現在添加下面方法:
複製代碼 代碼如下:
- (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;
}
}
}
手勢操作就如同傳入的一個參數,可以從 locationInView: 擷取定位資訊。
接下來,調用委託的 numberOfViewForHorizontalScroller: 方法。它必須遵照 HorizontalScrollerDelegate 的協議安全發送訊息,否則 HorizontalScroller 執行個體的代理是沒法使用這些資訊。
滾動視圖裡的每個視圖,用 CGRectContainsPoint 執行一個點擊測試,找到那個被點擊的視圖。當視圖被找到,發送給委託一個訊息 horizontalScroller:clickedViewAtIndex:。當你跳出這個迴圈後,設定被點擊的視圖滾動到視圖中間。
現在添加下面的代碼,用來重新整理滾動視圖(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];
}
}
能過代碼一步步來討論:
如果沒有代理,這裡什麼事情也不做。
移除之前添加的所有的子視圖。
給所有視圖設定一個位移(offset)位置。現在的是 100,但是通過頂部的 #define,它很容易修改。
HorizontalScroller 通過它的委託一次請求一個視圖,用之前定義的 padding 值把它們依次的一個個放置下來。
當所有的視圖都產生好,通過設定滾動視圖內容的位移量以達到使用者能過滾動可以看到所有專輯封面的目的。
HorizontalScroller 的委託需要驗證是否響應了 initialViewIndexForHorizontalScroller: 方法。這個驗證是必需的,因為這個特別的協議方法是可選性的。如果代理沒有執行這個方法,它的預設值會是 0。最終,通過委託,這塊代碼會在滾動視圖中間設定一個初始化好的視圖。
當資料發生改變的時候執行 reload 方法。當添加 HorizontalScroller 到別個一個視圖時,你同樣可以執行這個方法。在 HorizontalScroller.m 添加下面的代碼替換後面的方案:
複製代碼 代碼如下:
- (void)didMoveToSuperview
{
[self reload];
}
當它要添加一個子視圖的時候,didMoveToSuperview 會發送訊息給視圖。這時正好可以更新滾動視圖的內容。
HorizontalScroller 的最後一個難題就是,如何設定你看到的專輯總是在滾動視圖的中間。為了這些,當使用者通過他們的手指拖動滾動視圖的時候你就需要做一些計算了。
添加下面方法(同樣在 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];
}
上面的代碼通過滾動視圖的當前位移量,外觀尺寸,內邊距來計算當前視圖離中心的距離。最後一行非常重要:當一個視圖置中後,你需要通知委託你選擇的視圖改變了。
為了偵測使用者在滾動視圖內完成拖拽的動作,你需要添加 UIScrollViewDelegate 方法:
複製代碼 代碼如下:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate)
{
[self centerCurrentView];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self centerCurrentView];
}
當使用者完成拖拽的時候 scrollViewDidEndDragging:willDecelerate: 通知委託。如果滾動視圖沒有停止滾動, decelerate 參數會返回 true。當滾動結束,系統將會調用 scrollViewDidEndDecelerating。當使用者拖動滾動當前視圖後,兩種情況,我們都需要調用一個新方法來使當前視圖置中。
HorizontalScroller 現在可以使用了。瀏覽你剛剛寫的代碼;這裡沒有一處提到 Album 和 AlbumView 類。這非常棒,說明這個新的滾動視圖是真正的完全獨立的和可重用的。
Build 項目,確保所有的代碼編譯正確。
現在 HorizontalScroller 完成了,是時候在你的 APP 中使用了。開啟 ViewController.m 添加如下引用:
複製代碼 代碼如下:
#import “HorizontalScroller.h”
#import “AlbumView.h”
給 ViewController 添加 HorizontalScrollerDelegate:
複製代碼 代碼如下:
@interface ViewController () <UITableViewDataSource, UITableViewDelegate, HorizontalScroller>
在類的擴充裡為水平滾動視圖添加如下執行個體變數:
複製代碼 代碼如下:
HorizontalScroller *scroller;
現在你可以執行代理方法了;你會驚奇的發現只需要幾行代碼你就能實現很多功能。
在 ViewController.m 添加如下代碼:
複製代碼 代碼如下:
#pragma mark - HorizontalScrollerDelegate methods
- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index
{
currentAlbumIndex = index;
[self showDataForAlbumAtIndex:index];
}
這裡設定一個變數用來儲存當前的專輯,然後調用 showDataForAlbumAtIndex: 顯示一個新專輯的資料。
提示:一般在方法代碼的前面放置 #pragma mark 指示符。編譯器會忽略這一行,當你在使用 Xcode 的跳轉工具列(Xcode's jump bar)查看你的方法列表時,你會看到一個分隔字元和個加粗的指示標題。在 Xcode 裡,這可以協助你很容易的組織代碼。
下面,添加如下代碼:
複製代碼 代碼如下:
- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller *)scroller
{
return allAlbums.count;
}
這裡,協議方法返復原動視圖裡的視圖數量。因為滾動視圖需要顯示所有的專輯封面,這個 count 是所有專輯的數目。
現在,添加這些代碼:
複製代碼 代碼如下:
- (UIView *)horizontalScroller:(HorizontalScroller *)scroller viewAtIndex:(ini)index
{
Album *album = allAlbums[index];
return [[Album alloc] initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl];
}
這裡你建立了一個新 AlbumView,然後交給 HorizontalScroller 使用。
就是這樣,通過三個這麼短的方法就可以顯示一個漂亮的滾動視圖。
實際上,你仍需要建立一個真正的滾動視圖,然後添加到你的主視圖上,但是在這之前,先添加下面的方法:
複製代碼 代碼如下:
- (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 載入專輯資料,然後以當前視圖的索引值為基礎設定顯示當前的圖片。 如果當前視圖的索引小於零,意味著當前沒有選擇視圖,顯示列表裡的第一張專輯。否則顯示最後一張專輯。
現在,在 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];
上面的代碼建立了一個 HorizontalScroller 的執行個體,設定了它的背景顏色和委託,添加滾動視圖到主視圖上,在滾動視圖的子視圖上載入專輯資料。
提示:如果一個協議變得很大,裡面有很多方法,你應該考慮把它們分散到幾個小的協議裡去。UITableViewDelegate 和 UITableViewDataSource 就是一個很好的例子,因為它們都是 UITablveView 的協議。設計協議的時候,最好一個名稱引導一個功能。
構建和運行你的項目,你會看到一個新的很了不起的水平滾動視圖:
啊嗯,等等。水平滾動的視圖已經有了,可是專輯封面在哪裡?
對了,你還沒有代碼來執行下載圖片的功能。你需要添加一個下載圖片的方法。查檢 LibraryAPI 服務的所有介面,這裡需要添加一個新的方法。不管怎樣,現在還有幾件事情需要考慮:
AlbumView 並沒沒有通過 LibraryAPI 立即工作。你沒有給視圖添加通訊邏輯。
相同的原因,LibraryAPI 並不認識 AlbumView。
LibraryAPI 需要通知 AlbumView,一旦封面下載完成,AlbumView 就會顯示它。