iOS App設(shè)計(jì)模式開發(fā)之適配器模式使用的實(shí)戰(zhàn)演練
相信做App開發(fā)的同學(xué),對(duì)于一些第三方的統(tǒng)計(jì)分析、錯(cuò)誤收集等SDK應(yīng)該都不陌生。就目前而言市面上也有許多相同功能的產(chǎn)品,眼花繚亂,讓人無法抉擇選哪一款SDK才是最靠譜的。那就隨便先選一款試試用吧!
那么問題來了:如果項(xiàng)目都快做完了結(jié)果發(fā)現(xiàn)這款SDK實(shí)在坑爹,不僅擴(kuò)展性差,還經(jīng)常讓App Crash,那你是不是會(huì)想到替換掉這個(gè)SDK?
OK,那我們就換另一個(gè)試試,下載SDK下來,一看,傻眼了,設(shè)計(jì)風(fēng)格,封裝模塊完全不一樣,于是乎我們就到項(xiàng)目中全局搜索找到之前的SDK代碼干掉,然后重新再到各種地方用新的SDK來寫新的邏輯來替換,關(guān)鍵的是,中間還不知道會(huì)產(chǎn)生多少bug,漏掉多少未修改的代碼,總之始終會(huì)有一種不靠譜的感覺。
換一次還算好的,如果之后團(tuán)隊(duì)壯大了,這些數(shù)據(jù)分析之類的東西突然想自己做了,畢竟這些有價(jià)值的數(shù)據(jù)并不想這么拱手讓給一個(gè)第三方的公司嘛~這個(gè)時(shí)候你是不是就只想說:『呵呵』
所以這個(gè)時(shí)候適配器模式就起到作用了~
何為適配器模式
GoF對(duì)于適配器模式的解釋如下:
將一個(gè)類的接口轉(zhuǎn)換成客戶希望的另外一個(gè)接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些類可以在一起工作。
個(gè)人通俗理解:
適配器:顧名思義,將不兼容的轉(zhuǎn)換為兼容,如電源適配器,將全世界各種不相同的電壓轉(zhuǎn)換成相同的電壓輸出給目標(biāo)設(shè)備。
這里可以將目標(biāo)設(shè)備理解為『接口』,世界各種電壓可以理解為『產(chǎn)生相同功能的類』,電源適配器可以理解為『需要實(shí)現(xiàn)的適配器類』。
適配器模式產(chǎn)生的效果是:在不修改代碼或者修改極少代碼的情況下,快速的切換源(數(shù)據(jù)源、內(nèi)容源等)。
就像電源適配器一樣,去到不同國(guó)家,同一個(gè)設(shè)備只需要不同的電源適配器就可以使用當(dāng)前國(guó)家的電源,而不需要取拆卸機(jī)器。
使用真實(shí)場(chǎng)景
如文章開頭所講,被某盟的SDK坑了之后(確實(shí)在某些狀況下讓App Crash,產(chǎn)生原因初步判斷是濫用performSelector,不考慮對(duì)象被釋放的情況而產(chǎn)生的Crash),產(chǎn)生替換念想而思考,如果將來替換豈不是又要苦逼我們自己?
于是乎為了將來的輕松就必須動(dòng)動(dòng)腦子去設(shè)計(jì)代碼了,于是有了今天的適配器模式實(shí)戰(zhàn)。
如何使用適配器模式
一個(gè)適配器允許接口不兼容的類在一起工作。它把它自己包裹成一個(gè)對(duì)象,公開一個(gè)與這個(gè)對(duì)象相互作用的標(biāo)準(zhǔn)接口。
如果你熟習(xí)適配器模式,你會(huì)注意到蘋果實(shí)施它的時(shí)候有一點(diǎn)不同的習(xí)慣─蘋果使用協(xié)議 (protocols)。你可能熟習(xí)像 UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying 這樣的協(xié)議。例子,NSCopying 的協(xié)議 (protocol),任何類都可以提供這樣一個(gè)標(biāo)準(zhǔn)的復(fù)制方法。
我們提到的滾動(dòng)區(qū)域是這樣的:
現(xiàn)在開始,在項(xiàng)目導(dǎo)航的 View 文件夾上右擊鼠標(biāo),選擇 New File…,用 iOS\Cocoa Touch\Object-C class 模板創(chuàng)建一個(gè)新類。新類的名字叫 HorizontalScroller,選擇它的子類為 UIView。
打開 HorizontalScroller.h 文件在 @end 后面插入如下代碼:
@protocol HorizontalScrollerDelegate <NSObject>
// methods declaration goes in here
@end
這里定義一個(gè) HorizontalScrollerDelegate 名字的協(xié)議,它繼承于 NSObject 協(xié)議,同樣的這是繼承它父類的一個(gè) Objective-C 類。符合 NSObject 協(xié)議,這是一個(gè)很好的做法─或者遵照 NSObject 協(xié)議。這能使你從定義的 NSObject 發(fā)送消息到 HorizontalScroller 的代理。你將會(huì)看到為什么這很重要。
定義個(gè)代理執(zhí)行的方法,要在 @protocol 和 @end 之間,它們分為必要方法和可選方法。添加下面協(xié)議方法:
@required
// 詢問 delegate 在滾動(dòng)區(qū)域里有多少個(gè)視圖要被顯示
- (NSInteger)numberOfViewsForHorizontalScroller: (HorizontalScroller*)scroller;
// 返回索引是 index 的視圖
- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;
// 當(dāng)索引是 index 的視圖被點(diǎn)擊了,通知 delegate
- (void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index;
@optional
// 通知 delegate,顯示初始化時(shí)索引是 Index 的視圖。這個(gè)方法是可選的
// ask the delegate for the index of the initial view to display. this method is optional
// 如果沒有被 delegate 執(zhí)行,默認(rèn)值是 0
- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;
這里我們必選的和可選的方法我們都定義了。必選方法一定要被代理執(zhí)行,它通常包含一些類必須要執(zhí)行的數(shù)據(jù)。這里,必選方法是獲取視圖的數(shù)量,當(dāng)前顯示視圖的索引和當(dāng)視圖被點(diǎn)擊的時(shí)候執(zhí)行的操作??蛇x方法這里是初始化視圖;如果沒有執(zhí)行 HorizontalScroller 將會(huì)顯示第一個(gè)索引的視圖。
接下來,你需要在 HorizontalScroller 內(nèi)部定義你的新代理。但是協(xié)議的定義在類的定義下面,因此在這點(diǎn)上它是不可見的。你該怎么辦?
解決辦法就是在前面聲明協(xié)議以便于編譯器(和Xcode)知道這個(gè)協(xié)議是可用的。好了,在 @interface 上面加入下面代碼:
[/ode]
@protocol HorizontalScrollerDelegate;
[/code]
還是 HorizontalScroller.h,在 @interface 和 @end 之間加入下面代碼:
@property (weak) id<HorizontalScrollerDelegate> delegate;
- (void)reload;
這個(gè)屬性被定義成為一個(gè) weak。這是為了防止循環(huán) retain。如果一個(gè)類保持一個(gè)強(qiáng)指針(strong pointer)指向它的委托(delegate),同時(shí)委托也保持一個(gè)強(qiáng)指針指向這個(gè)類,在釋放類所占用的內(nèi)存時(shí)會(huì)造成 app 內(nèi)存泄漏。
id 的意思是把這個(gè)代理指定給一個(gè)類,它遵照 HorizontalScrollerDelegate,給你一些類型安全。
reload 方法是模仿 UITableView 類的 relaodData;它重新加載所有數(shù)據(jù)用來創(chuàng)建一個(gè)水平移動(dò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è)計(jì)時(shí)間可以方便修改布局。在滾動(dòng)視圖內(nèi),每個(gè)圖片的大小在一個(gè) 100×100 內(nèi)邊距為 10 點(diǎn)(point) 的矩形內(nèi)。
HorizontalScroller 遵照 UIScrollViewDelegate 協(xié)議。因?yàn)?HorizontalScroller 使用一個(gè) UIScrollView 來滾動(dòng)專輯封面,它需要知道用戶什么時(shí)候停止?jié)L動(dòng)。
創(chuàng)建一個(gè)包含圖片的滾動(dò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 將被滾動(dòng)視圖整個(gè)填充。如果一個(gè)專輯封面被點(diǎn)擊,UITapGestureRecognizer 將會(huì)監(jiān)聽它上面的事件。如果有,它會(huì)通知 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ì)操作就如同傳入的一個(gè)參數(shù),可以從 locationInView: 獲取定位信息。
接下來,調(diào)用委托的 numberOfViewForHorizontalScroller: 方法。它必須遵照 HorizontalScrollerDelegate 的協(xié)議安全發(fā)送消息,否則 HorizontalScroller 實(shí)例的代理是沒法使用這些信息。
滾動(dòng)視圖里的每個(gè)視圖,用 CGRectContainsPoint 執(zhí)行一個(gè)點(diǎn)擊測(cè)試,找到那個(gè)被點(diǎn)擊的視圖。當(dāng)視圖被找到,發(fā)送給委托一個(gè)消息 horizontalScroller:clickedViewAtIndex:。當(dāng)你跳出這個(gè)循環(huán)后,設(shè)置被點(diǎn)擊的視圖滾動(dòng)到視圖中間。
現(xiàn)在添加下面的代碼,用來刷新滾動(dòng)視圖(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è)置一個(gè)偏移(offset)位置。現(xiàn)在的是 100,但是通過頂部的 #define,它很容易修改。
HorizontalScroller 通過它的委托一次請(qǐng)求一個(gè)視圖,用之前定義的 padding 值把它們依次的一個(gè)個(gè)放置下來。
當(dāng)所有的視圖都生成好,通過設(shè)置滾動(dòng)視圖內(nèi)容的偏移量以達(dá)到用戶能過滾動(dòng)可以看到所有專輯封面的目的。
HorizontalScroller 的委托需要驗(yàn)證是否響應(yīng)了 initialViewIndexForHorizontalScroller: 方法。這個(gè)驗(yàn)證是必需的,因?yàn)檫@個(gè)特別的協(xié)議方法是可選性的。如果代理沒有執(zhí)行這個(gè)方法,它的默認(rèn)值會(huì)是 0。最終,通過委托,這塊代碼會(huì)在滾動(dòng)視圖中間設(shè)置一個(gè)初始化好的視圖。
當(dāng)數(shù)據(jù)發(fā)生改變的時(shí)候執(zhí)行 reload 方法。當(dāng)添加 HorizontalScroller 到別個(gè)一個(gè)視圖時(shí),你同樣可以執(zhí)行這個(gè)方法。在 HorizontalScroller.m 添加下面的代碼替換后面的方案:
- (void)didMoveToSuperview
{
[self reload];
}
當(dāng)它要添加一個(gè)子視圖的時(shí)候,didMoveToSuperview 會(huì)發(fā)送消息給視圖。這時(shí)正好可以更新滾動(dòng)視圖的內(nèi)容。
HorizontalScroller 的最后一個(gè)難題就是,如何設(shè)置你看到的專輯總是在滾動(dòng)視圖的中間。為了這些,當(dāng)用戶通過他們的手指拖動(dòng)滾動(dòng)視圖的時(shí)候你就需要做一些計(jì)算了。
添加下面方法(同樣在 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)視圖的當(dāng)前偏移量,外觀尺寸,內(nèi)邊距來計(jì)算當(dāng)前視圖離中心的距離。最后一行非常重要:當(dāng)一個(gè)視圖居中后,你需要通知委托你選擇的視圖改變了。
為了偵測(cè)用戶在滾動(dòng)視圖內(nèi)完成拖拽的動(dòng)作,你需要添加 UIScrollViewDelegate 方法:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate)
{
[self centerCurrentView];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self centerCurrentView];
}
當(dāng)用戶完成拖拽的時(shí)候 scrollViewDidEndDragging:willDecelerate: 通知委托。如果滾動(dòng)視圖沒有停止?jié)L動(dòng), decelerate 參數(shù)會(huì)返回 true。當(dāng)滾動(dòng)結(jié)束,系統(tǒng)將會(huì)調(diào)用 scrollViewDidEndDecelerating。當(dāng)用戶拖動(dòng)滾動(dòng)當(dāng)前視圖后,兩種情況,我們都需要調(diào)用一個(gè)新方法來使當(dāng)前視圖居中。
HorizontalScroller 現(xiàn)在可以使用了。瀏覽你剛剛寫的代碼;這里沒有一處提到 Album 和 AlbumView 類。這非常棒,說明這個(gè)新的滾動(dòng)視圖是真正的完全獨(dú)立的和可重用的。
Build 項(xiàng)目,確保所有的代碼編譯正確。
現(xiàn)在 HorizontalScroller 完成了,是時(shí)候在你的 APP 中使用了。打開 ViewController.m 添加如下引用:
#import “HorizontalScroller.h”
#import “AlbumView.h”
給 ViewController 添加 HorizontalScrollerDelegate:
@interface ViewController () <UITableViewDataSource, UITableViewDelegate, HorizontalScroller>
在類的擴(kuò)展里為水平滾動(dòng)視圖添加如下實(shí)例變量:
HorizontalScroller *scroller;
現(xiàn)在你可以執(zhí)行代理方法了;你會(huì)驚奇的發(fā)現(xiàn)只需要幾行代碼你就能實(shí)現(xiàn)很多功能。
在 ViewController.m 添加如下代碼:
#pragma mark - HorizontalScrollerDelegate methods
- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index
{
currentAlbumIndex = index;
[self showDataForAlbumAtIndex:index];
}
這里設(shè)置一個(gè)變量用來存儲(chǔ)當(dāng)前的專輯,然后調(diào)用 showDataForAlbumAtIndex: 顯示一個(gè)新專輯的數(shù)據(jù)。
提示:一般在方法代碼的前面放置 #pragma mark 指示符。編譯器會(huì)忽略這一行,當(dāng)你在使用 Xcode 的跳轉(zhuǎn)工具欄(Xcode's jump bar)查看你的方法列表時(shí),你會(huì)看到一個(gè)分隔符和個(gè)加粗的指示標(biāo)題。在 Xcode 里,這可以幫助你很容易的組織代碼。
下面,添加如下代碼:
- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller *)scroller
{
return allAlbums.count;
}
這里,協(xié)議方法返回滾動(dòng)視圖里的視圖數(shù)量。因?yàn)闈L動(dòng)視圖需要顯示所有的專輯封面,這個(gè) 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)建了一個(gè)新 AlbumView,然后交給 HorizontalScroller 使用。
就是這樣,通過三個(gè)這么短的方法就可以顯示一個(gè)漂亮的滾動(dòng)視圖。
實(shí)際上,你仍需要?jiǎng)?chuàng)建一個(gè)真正的滾動(dò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;
}
這個(gè)方法從 LibraryAPI 加載專輯數(shù)據(jù),然后以當(dāng)前視圖的索引值為基礎(chǔ)設(shè)置顯示當(dāng)前的圖片。 如果當(dāng)前視圖的索引小于零,意味著當(dāng)前沒有選擇視圖,顯示列表里的第一張專輯。否則顯示最后一張專輯。
現(xiàn)在,在 viewDidLoad 里 [self showDataForAlbumIndex:0] 前面添加下面代碼來初始化滾動(dòng)視圖:
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)建了一個(gè) HorizontalScroller 的實(shí)例,設(shè)置了它的背景顏色和委托,添加滾動(dòng)視圖到主視圖上,在滾動(dòng)視圖的子視圖上加載專輯數(shù)據(jù)。
提示:如果一個(gè)協(xié)議變得很大,里面有很多方法,你應(yīng)該考慮把它們分散到幾個(gè)小的協(xié)議里去。UITableViewDelegate 和 UITableViewDataSource 就是一個(gè)很好的例子,因?yàn)樗鼈兌际?UITablveView 的協(xié)議。設(shè)計(jì)協(xié)議的時(shí)候,最好一個(gè)名稱引導(dǎo)一個(gè)功能。
構(gòu)建和運(yùn)行你的項(xiàng)目,你會(huì)看到一個(gè)新的很了不起的水平滾動(dòng)視圖:
啊嗯,等等。水平滾動(dòng)的視圖已經(jīng)有了,可是專輯封面在哪里?
對(duì)了,你還沒有代碼來執(zhí)行下載圖片的功能。你需要添加一個(gè)下載圖片的方法。查檢 LibraryAPI 服務(wù)的所有接口,這里需要添加一個(gè)新的方法。不管怎樣,現(xiàn)在還有幾件事情需要考慮:
AlbumView 并沒沒有通過 LibraryAPI 立即工作。你沒有給視圖添加通信邏輯。
相同的原因,LibraryAPI 并不認(rèn)識(shí) AlbumView。
LibraryAPI 需要通知 AlbumView,一旦封面下載完成,AlbumView 就會(huì)顯示它。
相關(guān)文章
iOS NSNotificationCenter通知中心使用小結(jié)
IOS中經(jīng)常會(huì)使用到NSNotification和delegate來進(jìn)行一些類之間的消息傳遞,這篇文章主要介紹了iOS NSNotificationCenter使用小結(jié),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11ios動(dòng)態(tài)設(shè)置lbl文字標(biāo)簽的高度
本文給大家分享的是ios動(dòng)態(tài)設(shè)置lbl文字標(biāo)簽的高度寬度的方法,一共給大家匯總了3種方法,小伙伴們根據(jù)自己的項(xiàng)目需求自由選擇。2015-05-05iOS開發(fā)中使用UIWebView 屏蔽 alert警告框
這篇文章主要介紹了iOS開發(fā)中使用UIWebView 屏蔽 alert警告框的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11iOS中使用UISearchBar控件限制輸入字?jǐn)?shù)的實(shí)現(xiàn)方法
這篇文章主要介紹了iOS中使用UISearchBar控件限制輸入字?jǐn)?shù)的實(shí)現(xiàn)方法的相關(guān)資料,需要的朋友可以參考下2016-08-08iOS 本地視頻和網(wǎng)絡(luò)視頻流播放實(shí)例代碼
本篇文章主要介紹了iOS 本地視頻和網(wǎng)絡(luò)視頻流播放實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07