由 @krq_tiger(
http://weibo.com/xmuzyq
)翻译,如果你发现有什么错误,请与我联系谢谢。
适配器(Adapter)模式
适配器可以让一些接口不兼容的类一起工作。它包装一个对象然后暴漏一个标准的交互接口。
如果你熟悉适配器设计模式,苹果通过一个稍微不同的方式来实现它
-
苹果使用了协议的方式来实现。你可能已经熟悉
UITableViewDelegate, UIScrollViewDelegate, NSCoding
和
NSCopying
协议。举个例子,使用
NSCopying
协议,任何类都可以提供一个标准的
copy
方法。
如何使用适配器模式
前面提到的水平滚动视图如下图所示:
为了开始实现它,在工程导航视图中右键点击
View
组,选择
New File...
使用
iOS\Cocoa Touch\Objective-C class
模板创建一个类。命名这个新类为
HorizontalScroller,
并且设置它是
UIView
的子类。
打开
HorizontalScroller.h
文件,在
@end
行后面插入如下代码:
@protocolHorizontalScrollerDelegate <NSObject>
// methods declaration goes in here
@end
上面的代码定义了一个名为
HorizontalScrollerDelegate
的协议,它采用
Objective-C
类继承父类的方式继承自
NSObject
协议。去遵循
NSObject
协议或者遵循一个本身实现了
NSObject
协议的类
是一条最佳实践,这使得你可以给
HorizontalScroller
的委托发送
NSObject
定义的消息。你不久会意识到为什么这样做是重要的。
在
@protocol
和
@end
之间,你定义了委托必须实现以及可选的方法。所以增加下面的方法:
@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
行前面增加下面的代码即可:
@protocol HorizontalScrollerDelegate;
继续在
HorizontalScroller.h
文件中,在
@interface
和
@end
之间增加如下的语句:
@property (weak) id<HorizontalScrollerDelegate> delegate;
- (void)reload;
这里你声明属性为
weak.
这样做是为了防止循环引用。如果一个类强引用它的委托,它的委托也强引用那个类,那么你的
app
将会出现内存泄露,因为任何一个类都不能释放调分配给另一个类的内存。
id
意味着
delegate
属性可以用任何遵从
HorizontalScrollerDelegate
的类赋值,这样可以保障一定的类型安全。
reload
方法在
UITableView
的
reloadData
方法之后被调用,它重新加载所有的数据去构建水平滚动视图。
用如下的代码取代
HorizontalScroller.m
的内容:
#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
的实例。
下一步你需要实现初始化器。增加下面的代码:
- (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
的委托。
现在,增加下面的代码:
- (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:
消息。在退出循环之前,将被点击的视图放置到滚动视图的中间。
现在增加下面的代码去重新加载滚动视图:
- (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];
}];
// 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);
[scroller addSubview:view];
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:@selector(initialViewIndexForHorizontalScroller:)])
{
int initialView = [self.delegate initialViewIndexForHorizontalScroller:self];
[scroller setContentOffset:CGPointMake(initialView*(VIEW_DIMENSIONS+(2*VIEW_PADDING)), 0) animated:YES];
}
}
我们来一步步的分析代码中有注释的地方:
1.
如果没有委托,那么不需要做任何事情,仅仅返回即可。
2.
移除之前添加到滚动视图的子视图
3.
所有的视图的位置从给定的偏移量开始。当前的偏移量是
100
,它可以通过改变文件头部的
#DEFINE
来很容易的调整。
4.
HorizontalScroller
每次从委托请求视图对象,并且根据预先设置的边框来水平的放置这些视图。
5.
一旦所有视图都设置好了以后,设置
UIScrollerView
的内容偏移(
contentOffset
)以便用户可以滚动的查看所有的专辑封面。
6.
HorizontalScroller
检测是否委托实现了
initialViewIndexForHorizontalScroller:
方法,这个检测是需要的,因为这个方法是可选的。
如果委托没有实现这个方法,
0
就是缺省值。最后设置滚动视图为协议规定的初始化视图的中间。
当数据已经发生改变的时候,你要执行
reload
方法。当增加
HorizontalScroller
到另外一个视图的时候,你也需要调用
reload
方法。增加下面的代码来实现后面一种场景:
- (void)didMoveToSuperview
{
[self reload];
}
didMoveToSuperview
方法会在视图被增加到另外一个视图作为子视图的时候调用,这正式重新加载滚动视图的最佳时机。
最后我们需要确保所有你正在浏览的专辑数据总是在滚动视图的中间。为了这样做,当用户的手指拖动滚动视图的时候,你将需要做一些计算。
再一次在
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];
}
为了计算当前视图到中间的距离,上面的代码考虑了滚动视图当前的偏移量,视图的尺寸以及边框。最后一行代码是重要的,一当子视图被置中,你将需要将这种变化通知委托。
为了检测用户在滚动视图中的滚动,你必需增加如下的
UIScrollerViewDelegate
方法:
- (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
类的信息。这个相对的棒,因为这意味着这个新的滚动视图是完全的独立和可复用的。
构建的工程确保每个资源可以正确编译。
现在
HorizontalScroller
完整了,是时候去在
app
使用它了。打开
ViewController.m
增加下面的导入语句:
#import "HorizontalScroller.h"
#import "AlbumView.h"
增加
HorizontalScrollerDelegate
协议为
ViewController
遵循的协议:
@interfaceViewController ()<UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>
在类的扩展中增加下面的实例变量:
HorizontalScroller *scroller;
现在你可以实现委托方法;你可能会感到惊讶,因为只需要几行代码就可以实现大量的功能啦。
在
ViewController.m
中增加下面的代码:
#pragma mark - HorizontalScrollerDelegate methods
- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index
{
currentAlbumIndex = index;
[self showDataForAlbumAtIndex:index];
}
它设置保存当前专辑数据的变量,然后调用
showDataForAlbumAtIndex:
方法显示专辑数据。
注意
:
在
#pragma mark
指令后面写方法代码是一种通用的实践。
c
编译器会忽略调这些行,但是如果你通过
Xcode
的弹出框的时候,你将看到这些指令会帮你把代码组织成有独立和粗体标题的组。这可以帮你使得你的代码更方便在
Xcode
中导航。
接下来,增加下面的代码:
- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller
{
return allAlbums.count;
}
正如你意识到的,这个是返回滚动视图所有子视图数量的协议方法。因为滚动视图要显示所有专辑的封面,这个数量就是专辑记录的数量。
现在,增加下面的代码:
- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index
{
Album *album = allAlbums[index];
return [[AlbumView 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 >= allAlbums.count) currentAlbumIndex = allAlbums.count-1;
[scroller reload];
[self showDataForAlbumAtIndex:currentAlbumIndex];
}
这个方法通过
LibraryAPI
加载专辑数据,然后根据当前视图的索引设置当前显示的视图。如果当前的视图索引小于
0
,意味着当前没有选定任何视图,此时可以选择第一个专辑来显示,否则下面一个专辑将会显示。
现在在
viewDidLoad
的
[self showDataForAlbumAtIndex:0]
之前增加下面的代码来初始化滚动视图:
scroller = [[HorizontalScroller alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)];
scroller.backgroundColor = [UIColor colorWithRed:0.24f green:0.35f blue:0.49f alpha:1];
scroller.delegate = self;
[self.view addSubview:scroller];
[self reloadScroller];
上
面的代码简单的创建了一个
HorizontalScroller
类的实例,设置它的背景色,委托,增加它到主视图,然后加载所有子视图去显示专辑数据。
注意
:
如果一个协议变得特别冗长,包含太多的方法。你应该考虑将它氛围更家细粒度的协议。
UITableViewDelegate
和
UITableViewDataSource
是一个好的例子。因为它们都是
UITableView
的协议。试着设计你的协议以便每个协议都关注特定的功能。
构建并运行你的
on
过程,查看一下你帅气十足的水平滚动视图吧:
对了,等等。水平滚动视图没问题,但是为什么没有显示封面呢?
是的,那就对了
-
你还没有实现下载封面的代码。为了实现这个功能,你需要去新增一个下载图片的方法。因为所有对服务的访问都通过
LibraryAPI,
那我们就可以在
LibraryAPI
中实现新的方法。然而我们首先需要虑一些事情:
1.
AlbumView
不应该直接和
LibraryAPI
交互。你不想混淆显示逻辑和网络交互逻辑。
2.
同样的原因,
LibraryAPI
也不应该知道
AlbumView
。
3.
一旦封面已经下载,
LibraryAPI
需要通知
AlbumView
,因为
AlbumView
显示专辑封面。
听上去是不是挺糊涂的?不要灰心。你将学习如何使用观察者模式来实现它。
原文出处:http://xmuzyq.iteye.com/blog/1942381