IOS设计模式第六篇之适配器设计模式
版权声明:原创作品,谢绝转载!否则将追究法律责任。
那么怎么使用适配器设计模式呢?
这个之前提到的水平滚动的视图像这样:
为了开始实现他,我们创建一个新的继承与UIView的HorizontalScroller 类。打开头文件添加以下代码:
@protocol HorizontalScrollerDelegate <NSObject>
// methods declaration goes in here
@end
这里定义了一个叫做HorizontalScrollerDelegate的协议并且他继承与NSObject协议就像Objective-c继承与他的父类一样。符合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 类的定义引用新的代理。但是这个协议定义在类定义之下,所以在这一点上不可见,你能做什么。
这个解决方法是向前声明协议让编译器知道这个协议是可用的。为了这样做在@interface上面加上
@protocol HorizontalScrollerDelegate;
在@interface和@end之间添加下面代码:
@property (weak) id<HorizontalScrollerDelegate> delegate;
- (void)reload;
这个属性的关键字是weak。目的是阻止循环引用。如果一个类保持了一个强引用向代理,并且代理保持了一个强引用向这个类,那么应用程序将会造成内存泄漏,无论是类将释放分配的内存。
id意味着这个代理可以是符合这个HorizontalScrollerDelegate的任何类。给你一些类型安全。
这个reload方法类似tableView的reloadData。他重新加载数据来构建水平滑动视图。
在HorizontalScroller实现文件里添加下面代码:
#import "HorizontalScroller.h"
// 1#define VIEW_PADDING 10#define VIEW_DIMENSIONS 100#define VIEWS_OFFSET 100
// 2@interface HorizontalScroller () <UIScrollViewDelegate>
@end
// 3@implementation HorizontalScroller
{
UIScrollView *scroller;
}
@end
解释下每个步骤:
定义一个常量便于修改布局。
HorizontalScroller 符合这个UIScrollViewDelegate 协议因此HorizontalScroller 用UIScrollView 来滚动专辑。他需要知道用户的活动比如当用户停止滚动。
创建一个UIScrollView 来包含一些视图。
下一步你需要实现初始化:
- (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;
}
这个UIScrollView 完全充满HorizontalScroller。一个UITapGestureRecognizer 检测手势并且检测一个专辑被点击。现在添加这个方法:
- (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 协议。
UIScrollView的没一个视图用CGRectContainsPoint 方法检测是不是被点击。当这个视图找到的时候就向代理发送 horizontalScroller:clickedViewAtIndex: 消息。在跳出这个循环时候,你必须让被点击的视图在UIScrollView中间。
现在添加下面代码来重新加载UIScrollView:
- (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:移除掉以前添加到UIScrollView上的子视图
3:所有的视图被定位从被给定的偏移量。当前是100.你也可以很容易的改变上面的常量的定义。
4:这个HorizontalScroller 访问他代理的视图并让他们按照之前水平布局的那样填充。
5:一旦所有的视图在合适的位置,设置内容的偏移量为UIScrollView让用户能浏览所有的专辑封面
6:这个HorizontalScroller 检测如果他的代理能响应initialViewIndexForHorizontalScroller:选择器。这个检测是必须的因为这个特殊的协议方法是可选的。如果代理不能实现这个方法。那么0就是默认的值。最后这段代码设置UIScrollView让代理定义的这个初始视图居中。
你执行这个reload当你数据需要改变的时候。你添加HorizontalScroller 到另一个视图也得调用这个方法因此在HorizontalScroller 的实现文件添加下面代码:
- (void)didMoveToSuperview
{
[self reload];
}
didMoveToSuperview 消息被发送到一个视图当他被添加到另一个视图作为子视图。这个是重新加载数据的好时机。
最后一段代码确保你的专辑视图一直在UIScrollView的中间位置,这样做的,你需要做一些计算当用手指拖拽UIScrollView的时候。
添加下面方法到HorizontalScroller实现文件:
- (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];
}
上面代码考虑当前UIScrollView的偏移量和视图的填充和尺寸目的是计算当前视图到中间的距离。最后一行很重要:一旦这个视图在中间,然后你通知代理这个选中的视图被改变了。
为了检测用户完成UIScrollView的拖拽,你必须添加
UIScrollViewDelegate的协议方法:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate)
{
[self centerCurrentView];
}}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self centerCurrentView];
}
scrollViewDidEndDragging:willDecelerate:通知代理当用户完成拖拽。这个decelerate参数是真如果UIScrollView没有完全停止。当滚动动作结束,系统调用
scrollViewDidEndDecelerating。这两个例子我们都应该调用新方法来使当前视图居中因为当前视图可能因为用户的拖拽而改变。
现在这个HorizontalScroller 准备好了,浏览之前写的代码。你将看到没有提及Album 和AlbumView类。这是很好的机制因为意味着你的这个新的滚动视图是独立并且可重用的。
编译运行你的项目确保代码是正确的。
现在这个HorizontalScroller 是完成了。现在打开
ViewController实现文件添加下面代码:
#import "HorizontalScroller.h"#import "AlbumView.h"
添加HorizontalScrollerDelegate到Viewcontroller里面让他符合则个协议。
@interface ViewController ()<UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>
在延展里面添加Horizontal Scroller的一个实例变量。
现在你可以实现代理方法,你会发现仅仅几行就实现了很多功能在Viewcontroller实现文件里面添加下面代码:
#pragma mark - HorizontalScrollerDelegate methods- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index
{
currentAlbumIndex = index;
[self showDataForAlbumAtIndex:index];
}
设置这个实例变量存储当前的专辑调用showDataForAlbumAtIndex:来展示新的专辑。
注意:这是常见的用法将方法组织起来然后用#pragma mark指令。这个编译器将忽略这一行但如果你通过xcode的下拉列表的方法直接跳转到相应的方法。你会看到这个指令的一个分割线和标题。这样便于你在xcode中组织管理你的代码。
下一步添加下列代码:- (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添加下面方法: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的协议。试着设计你的协议使他们来处理一个特定区域的功能。
编译运行你的项目来看看你的新的滑动视图。
你现在没有添加代码来下载图片,现在还需要添加一个下载图片的方法。因为所有的方法服务是通过libraryAPI,这就是一个新的方法,然后需要有几点事情要考虑。
1:AlbumView 不能直接和libraryAPI交互。你不能混淆你的视图逻辑和通讯逻辑。
2:同样的原因libraryAPI不知道AlbumView
3:libraryAPI需要通知AlbumView 一旦图片下载并展示到视图上。
这就是我们下面介绍的观察者设计模式。