相信做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 就會顯示它。