适配器模式让不同的类之间的不兼容的接口可以一起工作。它将自己包装成一个对象,然后暴露一个标准的接口去让外界和这个对象去交互。
如果你对适配器模式熟悉,那么你会注意到苹果用一个稍微不同的方法去实现它-苹果使用协议去做这个工作,你也许会熟悉像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要去显示它们。