适配器(Adapter)模式
适配器可以让一些接口不兼容的类一起工作。它包装一个对象然后暴漏一个标准的交互接口。
如果你熟悉适配器设计模式,苹果通过一个稍微不同的方式来实现它,苹果使用了协议的方式来实现。你可能已经熟悉UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying协议。举个例子,使用NSCopying协议,任何类都可以提供一个标准的copy方法。
如何使用适配器模式
前面提到的水平滚动视图如下图所示:
为了开始实现它,在工程导航视图中右键点击View组,选择New File…使用iOS\Cocoa Touch\Objective-C class 模板创建一个类。命名这个新类为HorizontalScroller,并且设置它是UIView的子类。
打开HorizontalScroller.h文件,在@end 行后面插入如下代码:
1 2 3 |
@protocolHorizontalScrollerDelegate // methods declaration goes in here @end |
上面的代码定义了一个名为HorizontalScrollerDelegate的协议,它采用Objective-C 类继承父类的方式继承自NSObject协议。去遵循NSObject协议或者遵循一个本身实现了NSObject协议的类 是一条最佳实践,这使得你可以给HorizontalScroller的委托发送NSObject定义的消息。你不久会意识到为什么这样做是重要的。
在@protocol和@end之间,你定义了委托必须实现以及可选的方法。所以增加下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@required // ask the delegate how many views he wants to present inside the horizontal scroller - (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller; // ask the delegate to return the view that should appear at <index> - (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index; // inform the delegate what the view at <index> has been clicked - (void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index; @optional // ask the delegate for the index of the initial view to display. this method is optional // and defaults to 0 if it's not implemented by the delegate - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller; |
这里你既有必需的方法也有可选方法。必需的方法要求委托必须实现它,因为它提供一些必需的数据。在这里,必需的是视图的数量,指定索引位置的视图,以及用户点击视图后的行为,可选的方法是初始化视图;如果它没有实现,那么HorizontalScroller将缺省用第一个索引的视图。
下一步,你需要在HorizontalScroller类中引用新建的委托。但是委托的定义是在类的定义之后的,所以在类中它是不可见的,怎么办呢?
解决方案就是前置声明委托协议以便编译器(和Xcode)知道协议的存在。如何做?你只需要在@interface行前面增加下面的代码即可:
1 |
@protocolHorizontalScrollerDelegate; |
继续在HorizontalScroller.h文件中,在@interface 和@end之间增加如下的语句:
1 2 |
@property (weak) id delegate; - (void)reload; |
这里你声明属性为weak.这样做是为了防止循环引用。如果一个类强引用它的委托,它的委托也强引用那个类,那么你的app将会出现内存泄露,因为任何一个类都不能释放调分配给另一个类的内存。
id意味着delegate属性可以用任何遵从HorizontalScrollerDelegate的类赋值,这样可以保障一定的类型安全。
reload方法在UITableView的reloadData方法之后被调用,它重新加载所有的数据去构建水平滚动视图。
用如下的代码取代HorizontalScroller.m的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#import "HorizontalScroller.h" // 1 #define VIEW_PADDING 10 #define VIEW_DIMENSIONS 100 #define VIEWS_OFFSET 100 // 2 @interfaceHorizontalScroller () <UIScrollViewDelegate> @end // 3 @implementationHorizontalScroller { UIScrollView *scroller; } @end |
让我们来对上面每个注释块的内容进行一一分析:
- 1. 定义了一系列的常量以方便在设计的时候修改视图的布局。水平滚动视图中的每个子视图都将是100*100,10点的边框的矩形.
- 2. HorizontalScroller遵循UIScrollViewDelegate协议。因为HorizontalScroller使用UIScrollerView去滚动专辑封面,所以它需要用户停止滚动类似的事件
- 3.创建了UIScrollerView的实例。
下一步你需要实现初始化器。增加下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { scroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; scroller.delegate = self; [self addSubview:scroller]; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollerTapped:)]; [scroller addGestureRecognizer:tapRecognizer]; } return self; } |
滚动视图完全充满了HorizontalScroller。UITapGestureRecognizer检测滚动视图的触摸事件,它将检测专辑封面是否被点击了。如果专辑封面被点击了,它会通知HorizontalScroller的委托。
现在,增加下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
- (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 subviews that we added for (int index=0; index<[self.delegate numberOfViewsForHorizontalScroller: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; } } } |
Gesture对象被当做参数传递,让你通过locationInView:导出点击的位置。
接下来,你调用了numberOfViewsForHorizontalScroller:委托方法,HorizontalScroller实例除了知道它可以安全的发送这个消息给委托之外,它不知道其它关于委托的信息,因为委托必须遵循HorizontalScrollerDelegate协议。
对于滚动视图中的每个子视图,通过CGRectContainsPoint方法发现被点击的视图。当你已经找到了被点击的视图,给委托发送horizontalScroller:clickedViewAtIndex:消息。在退出循环之前,将被点击的视图放置到滚动视图的中间。
现在增加下面的代码去重新加载滚动视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
- (void)reload { // 1 - nothing to load if there's no delegate if (self.delegate == nil) return; // 2 - remove all subviews [scroller.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [obj removeFromSuperview]; } |