適配器設計模式(The Adapter Pattern)
適配器模式讓不同的類之間的不兼容的接口可以一起工作。它將自己包裝成一個對象,然後暴露一個標準的接口去讓外界和這個對象去交互。
如果你對適配器模式熟悉,那麼你會注意到蘋果用一個稍微不同的方法去實現它-蘋果使用協議去做這個工作,你也許會熟悉像UITableViewDelegate,UIScrollViewDelegate,NSCoding,NSCopying這樣的協議,例如,通過NSCopying協議,任何的類都可以提供一個標準的copy方法。
如何使用適配器模式
這個之間提到的horizontal scroller應該是像下面的這個圖這樣子。
首先新建一個Objective-C的類,讓它繼承於UIView,打開它的.h文件,在@end的下面寫上這行代碼。
@protocolHorizontalScrollerDelegate <NSObject> @end
這定義了一個名爲HorizontalScrollerDelegate的協議,繼承於 NSObject協議。這是一個很好的實踐去遵從NSObject協議-或者去遵從一個已經遵從NSObject協議的協議,這會讓你可以向HorizontalScroller的代理對象發送NSObject中定義的消息。你將會看到這爲什麼是那麼的重要。
你要定義它的代理必須和可選實現的方法在@protocol和@end之間
@required - (int)numberOfviewsForHorizontalScroller:(HorizontalScroller *)horizontalScroller; - (UIView *)horizontalScroller:(HorizontalScroller *)scrollerviewAtIndex:(int)index; - (void)horizontalScroller:(HorizontalScroller *)scrollerclickViewAtIndex:(int)index; @optional - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller;
在這裏你既有可選的也有必須的方法,必須的方法必須被代理實現,
而通常這回包含一些這個類絕對需要的數據。在這個例子中,這些必須的分別是視圖的數量,在特定的位置的視圖和當一個視圖被點擊後的行爲。這個可選的方法是
初始化的使用,如果它不被代理所實現,那麼默認就是第一個視圖。
接下來,你需要將這個類的定義飲用到你的代理中去。但是這個協議的定義是在累的定義之下的,因此還剩不可見的,那你該怎麼辦呢。
解決的辦法奇偶說前向的定義一個協議來讓編譯器去知道這樣一個協議是可用的,所以,添加這行代碼在@interface的上方。
然後在@interface和@end之間下入如下代碼。
這個代理的屬性是weak類型的,爲了防止循環引用這是必須的,如果一個類持有一個強指針指向它的代理,而它的代理也持有一個強的指針指向它,那麼你的應用會因爲任何一個類都不能彼此釋放內存而造成內存泄漏。而id類型表示你只可以成爲遵守HorizontalScrollerDelefate的對象的assign方,給了你一定程度上的類型安全。
這個reload方法是一個在UITableView之後被重新刷新了,它reload了所有的用於構建horizontal scroller的數據。
用下面的代碼體大地HorizontalScroller.m中所有的代碼。
#import "HorizontalScroller.h" #define VIEW_PADDING 10 #define VIEW_DIMENSION 100 #define VIEW_OFFSET 100 @interfaceHorizontalScroller ()<UIScrollViewDelegate> { UIScrollView*scroller; } @end @implementationHorizontalScroller @end
看一下這些註釋:
1. 定義了一些常量去使更容易在設計時修改佈局。視圖在這個scroller中的面積是100*100有一個和它相近的矩形有一個10。
2. HorizontalScroller遵守<UIScrollViewDelegate>,這是因爲HorizontalScroller適應一個UIScrollView去滾動album,它需要去知道用戶的行爲比如用戶停止了滾動。
3. 創建了一個scroll view容器。
下一步你需要去實現這個初始化方法,添加下面這個方法。
- (id)initWithFrame:(CGRect)frame { self = [superinitWithFrame:frame]; if (self) { scroller = [[UIScrollViewalloc]initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; scroller.delegate = self; [selfaddSubview:scroller]; UITapGestureRecognizer *tap = [[UITapGestureRecognizeralloc]initWithTarget:selfaction:@selector(tapAction:)]; [scrolleraddGestureRecognizer:tap]; } returnself; }
這個scroll view完全填充了這個HorizontalScroller,一個UITapGestureRecognizer檢測有沒有在這個scroll view上有觸摸行爲和檢查一個album cover是否被點擊。如果有的話,它會通知HorizontalScroller的代理。加入這個方法。
- (void)tapAction:(UITapGestureRecognizer *)gestureRecognizer; { CGPoint location =[gestureRecognizer locationInView:gestureRecognizer.view]; for (int index = 0; index< [self.delegatenumberOfviewsForHorizontalScroller:self]; index++) { UIView *view = scroller.subviews[index]; if (CGRectContainsPoint(view.frame, location)) { [self.delegatehorizontalScroller:selfclickViewAtIndex:index]; [scrollersetContentOffset:CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0)]; break; } } }
手勢被當成locationInView的一個參數去傳遞讓你精確的知道位置,接下來你喚醒了它的代理的numberOfviewsForHorizontalScroller方法,這個HorizontalScroller的實例除了知道它可以安全的向一個遵守了HorizontalScrollerDelegate的對象發送方法以外其他的一無所知。對於在uiscroll view中的每一個視圖,用CGRectContainsPoint方法去找出被點擊的視圖。當這個視圖找到了以後,向它的代理髮送clickViewAtIndex消息。在不跳出這個循環之前,將這個被點擊的視圖居中。添加下面這個方法去reload這個scroller。
- (void)reload { if (self.delegate == nil) { return; } [scroller.subviewsenumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOLBOOL *stop) { [obj removeFromSuperview]; }]; // 3 - xValue is the starting point of theviews inside the scroller CGFloat xValue = VIEW_OFFSET; for (int i=0; i<[self.delegatenumberOfviewsForHorizontalScroller:self]; i++) { // 4 - add a view at theright position xValue += VIEW_PADDING; UIView *view = [self.delegatehorizontalScroller:selfviewAtIndex:i]; view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSION, VIEW_DIMENSION); [scrolleraddSubview:view]; xValue += VIEW_DIMENSION+VIEW_PADDING; } // 5 [scrollersetContentSize:CGSizeMake(xValue+VIEW_OFFSET, self.frame.size.height)]; if ([self.delegaterespondsToSelector:@selector(initialViewIndexForHorizontalScroller:)]){ int initialViewIndex = [self.delegateinitialViewIndexForHorizontalScroller:self]; [scrollersetContentOffset:CGPointMake(initialViewIndex*(VIEW_DIMENSION+(2*VIEW_PADDING)), 0) animated:YES]; } }
通過逐行註釋來看看這些代碼。
1. 如果沒用代理,那麼沒用什麼可以做的事情,那你可以直接返回了。
2. 將原先添加到scroll view中的字視圖全部移除掉。
3. 所有的視圖都在一個給丁的距離開始,現在它是100,但是可以輕鬆的通過改變上面那個定義的常量去改變,
4. HorizontalScroller每次詢問它的代理讓它們彼此水平的挨着在一塊。
5. 一旦所有的視圖都安置好了,設置這個滾動視圖的contenOffset讓用戶可以滾動所有的album covers。
6. 這個HorizontalScroller檢查它的代理是否響應initialViewIndexForHorizontalScroller這個方法,如果響應的話這個代碼就會將這個滾動視圖放在它的代理定義的初始視圖的中心,否則默認的就是0
當你的數據發生了改變以後你要執行reload操作,你也可以執行調用這個方法在你將HorizontalScroller添加到其他的視圖上的時候。加入一下代碼到HorizontalScroller.m文件中,
- (void)didMoveToSuperview { [selfreload]; }
didMoveToSuperview這個消息當一個視圖要添加到另一個視圖上作爲一個子視圖上時被調用。這個時候就是reload scroller的內容的正確的時機了。HorizontalScroller的最後一個難題時確保你正在看見的album總是在scroll view的正中間。因此你們要去實現一些計算當用戶用手指拖動這個scroll view時。
將這個代碼添加到HorizontalScroller.m中
- (void)centerCurrentView { int xFinal = scroller.contentOffset.x + (VIEW_OFFSET/2) + VIEW_PADDING; int viewIndex =xFinal / (VIEW_DIMENSION+(2*VIEW_PADDING)); xFinal =viewIndex * (VIEW_DIMENSION+(2*VIEW_PADDING)); [scrollersetContentOffset:CGPointMake(xFinal,0) animated:YES]; [self.delegatehorizontalScroller:selfclickViewAtIndex:viewIndex]; }
上面這段代碼計算當前的scroll view的騙一直和麪積和視圖的padding去計算當前的視圖離中心的距離。最後一行是很重要的,一旦這個視圖移動到中心了,通知它的代理這個顯示的視圖被改變了。
爲了檢測用戶在scroll view的拖動,你必須添加如下的UIScrollViewDelegate方法:
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView { [selfcenterCurrentView]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollViewwillDecelerate:(BOOL)decelerate { if (!decelerate) { [selfcenterCurrentView]; } }
scrollViewDidEndDragging: willDecelerate:會通知它的代理當用戶結束拖動的時候。如果用戶還沒有結束拖動那麼這個decelerate參數就爲真。當用戶結束拖動,那麼系統就會調用前面那個方法。兩個方法中我們都調用了新的方法去讓當前的視圖居中因爲這個當前的視圖已經在用戶拖動後發生了改變。
現在這個HorizontalScroller已經準備好使用了。回顧一下你剛纔所寫的代碼,一點都沒有提及到album和albumView這些類,這是非常好的,因爲這意味着你的代碼是獨立的和可重用的。
現在編譯使工程確保沒事。
現在HorizontalScroller已經準備好了,是時候去使用它了。打開viewController.m加入如下的代碼:
#import "HorizontalScroller.h" #import "AlbumView.h"
添加HorizontalScrollerDelegate,使
@interfaceViewController () <UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>
添加下面的這個實例變量到它的類擴展中。 HorizontalScroller *scroller;
現在你可以實現這些代理方法了,你將會驚訝只用幾行代碼就可以實現很多的功能。
添加下面的代碼到ViewController.m中
- (void)horizontalScroller:(HorizontalScroller *)scrollerclickViewAtIndex:(int)index { currentAlbumIndex = index; [selfshowDataForAlbumAtIndex:currentAlbumIndex]; }
這裏設置了這些變量去存儲暫時的album然後調用showDataForAlbumAtIndex方法去顯示一個下那的album的數據。
小貼士:將一些方法放在一起通過@pragma mark是一種很好的習慣。編譯器會忽略這一行但是你會在你的Xcode的jump bar中將這些方法列起來。這會幫助你在Xcode中更好的組織代碼。然後添加下面的代碼
-(int)numberOfviewsForHorizontalScroller:(HorizontalScroller *)horizontalScroller { return [allAlbumscount]; }
這就是像你能辨認出來的那樣,這是協議方法返回在scroll view中的視圖的個數。因爲摺合scorll view爲所有的album的數據顯示covers ,這個count就是album記錄的數據。接下來添加這個方法:
-(UIView *)horizontalScroller:(HorizontalScroller *)scrollerviewAtIndex:(int)index { Album *album = [allAlbumsobjectAtIndex:index]; return [[AlbumViewalloc]initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl]; }
在這裏你創建了一個新的AlbumView然後將它傳遞給了horizontalScroller.
就是這麼多,你只用了三個很簡短的方法就顯示了一個很好看的horizontalScroller。
是的,你仍要去真正的創建一個scroller然後將它添加到你的主要的view中但是在坐這個之前。添加這個方法:
- (void)reloadScroller { // allAlbums = [[LibraryAPI sharedInstance]getAlbums]; if (currentAlbumIndex < 0) { currentAlbumIndex = 0; }elseif(currentAlbumIndex >= [allAlbumscount]){ currentAlbumIndex = [allAlbumscount]-1; } [scrollerreload]; [selfshowDataForAlbumAtIndex:currentAlbumIndex]; }
這個方法loads album的數據通過LibraryAPI然後設置當前啊的現實的視圖在機遇當前的視圖的index的值上,如果當前的view index 小於0,那麼意味着沒有當前的視圖選擇,那就將第一張album顯示,否則就是最後一張album被顯示。
現在初始化這個scroller通過添加下面的代碼到你的viewController.m中
scroller = [[HorizontalScrolleralloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)]; scroller.delegate = self; [self.viewaddSubview:scroller]; [selfreloadScroller];
上面的僅僅是創建了一個新的HorizontalScroller實例,添加到main view中,然後loads所有的子視圖去顯示album的數據。
小貼士:如果一個協議變得很大,然後有很多的方法,那麼你應該考慮將它分解成幾個小的協議,UITableViewDelegate和UITableViewDataSource就是一個非常好的例子。嘗試着去設計你的戲而已讓每一個控制一個具體的功能。
編譯和創建米的工程,看一下你的這個很棒的新的horizontal scroller:如下圖:
等下,這個horizontal scroller是在裏面了,但是這個album cover在哪裏呢?
額,那九堆了-你還沒有去實現下載cover的代碼。所以你將要去添加一個下載圖片的方法,計算你的所以訪問的服務都是通過LibraryAPI,那麼那裏就是你放這些方法的地方,然後,首先還要考慮以下幾個問題:
1 AlbumView不應該直接和LibraryAPI去搭配工作,你不要去將視圖邏輯和交流邏輯混合起來.
2 相同的原因,LibraryAPI也不該知道有關AlbumView的事情。
3 LibraryAPI應該去通知AlbumView一旦這些covers已經下載好了因爲AlbumView要去顯示它們。